diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f69351 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ + +# Virtual environment +.venv/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Streamlit +.streamlit_cache/ + +# Ruff +.ruff_cache/ + +# UV +*.lock + +# OS +.DS_Store +Thumbs.db +/.doc/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5fce0e5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +# 使用 uv 官方镜像作为基础镜像(已包含 Python 3.14 和 uv) +FROM 172.16.102.3:30648/astral-sh/uv:python3.14-bookworm + +# 设置工作目录 +WORKDIR /app + +# 配置 apt 使用阿里云镜像源 +RUN sed -i 's/httpredir.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources + +# 安装系统依赖 +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# 复制项目配置文件和锁定文件 +COPY pyproject.toml justfile uv.lock ./ + +# 配置 uv 使用阿里云镜像源(通过环境变量) +ENV UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ + +# 安装 Python 依赖(使用 uv,锁定版本) +RUN uv sync --frozen --no-dev + +# 复制应用代码和其他文件 +COPY . . + +# 暴露 Streamlit 默认端口 +EXPOSE 8000 + +# 设置环境变量 +ENV STREAMLIT_SERVER_PORT=8000 \ + STREAMLIT_SERVER_ADDRESS=0.0.0.0 \ + STREAMLIT_SERVER_ENABLE_XSRF_PROTECTION=false \ + STREAMLIT_SERVER_ENABLE_CORS=false \ + STREAMLIT_SERVER_HEADLESS=true \ + STREAMLIT_BROWSER_GATHER_USAGE_STATS=false + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/_stcore/health || exit 1 + +# 运行 Streamlit 应用 +CMD ["uv", "run", "streamlit", "run", "app.py", "--server.port=8000", "--server.address=0.0.0.0"] diff --git a/README.md b/README.md index 8fe876d..1ba4394 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,89 @@ -# yield-smart-app +# YieldSmart 农业智能决策系统 -农业智能决策系统 \ No newline at end of file +基于多因子 Cobb-Douglas 产量模型的作物种植决策支持应用。 + +## 功能特性 + +- 🌾 多作物产量预测(水稻、小麦、玉米、大豆、油菜、棉花) +- 📊 影响因子雷达图可视化 +- 🏅 作物推荐智能排行 +- 📈 产量敏感性分析(氮肥/降雨量) +- 💡 智能种植建议与环境匹配评估 + +## 技术栈 + +- Python 3.14+ +- Streamlit 1.52.1 +- Plotly 6.5.0 +- Pandas 2.3.3 +- NumPy 2.3.5 + +## 快速开始 + +### 使用 uv(推荐) + +```bash +# 安装依赖 +uv sync + +# 运行应用 +uv run streamlit run app.py +``` + +### 使用传统方式 + +```bash +# 创建虚拟环境 +python -m venv .venv +source .venv/bin/activate + +# 安装依赖 +pip install -r requirements.txt + +# 运行应用 +streamlit run app.py +``` + +## 项目结构 + +``` +YieldSmart/ +├── app.py # 主应用文件(Streamlit) +├── main.py # 入口文件 +├── pyproject.toml # 项目配置 +├── justfile # 任务自动化 +├── Dockerfile # Docker 配置 +└── README.md # 项目文档 +``` + +## 使用 just + +项目使用 justfile 进行任务管理: + +```bash +# 查看所有可用命令 +just --list + +# 运行应用 +just run + +# 代码格式化 +just format + +# 代码检查 +just check +``` + +## Docker 部署 + +```bash +# 构建镜像 +docker build -t yieldsmart . + +# 运行容器 +docker run -p 8000:8000 yieldsmart +``` + +## 许可证 + +MIT License diff --git a/app.py b/app.py new file mode 100644 index 0000000..973643a --- /dev/null +++ b/app.py @@ -0,0 +1,587 @@ +""" +农业智能决策系统 +基于多因子 Cobb-Douglas 产量模型的作物种植决策支持应用 +""" + +import streamlit as st +import numpy as np +import pandas as pd +import plotly.graph_objects as go +import plotly.express as px +from plotly.subplots import make_subplots + +# ─── Page Config ──────────────────────────────────────────────────────────── +st.set_page_config( + page_title="农业智能决策系统", + page_icon="🌾", + layout="wide", + initial_sidebar_state="expanded", +) + +# ─── Custom CSS ────────────────────────────────────────────────────────────── +st.markdown(""" + +""", 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": "#4ade80" + }, + "小麦": { + "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": "#f59e0b" + }, + "玉米": { + "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": "#fbbf24" + }, + "大豆": { + "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": "#a78bfa" + }, + "油菜": { + "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": "#f97316" + }, + "棉花": { + "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": "#e2e8f0" + }, +} + + +# ─── 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.markdown('
🌾 农业决策
', unsafe_allow_html=True) + st.markdown('
SMART FARMING SYSTEM v2.0
', unsafe_allow_html=True) + st.markdown("---") + + st.markdown('
🧪 土壤参数
', unsafe_allow_html=True) + 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.markdown('
🌦 气象数据
', unsafe_allow_html=True) + 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.markdown('
🌱 种植参数
', unsafe_allow_html=True) + area = st.number_input("种植面积 (公顷)", 0.1, 10000.0, 100.0, 10.0) + pesticide = st.slider("农药用量 (kg/ha)", 0, 200, 50, 5) + + st.markdown('
🎯 目标作物
', unsafe_allow_html=True) + 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.markdown(f""" +
+
农业智能决策系统
+
+
YIELD = f(SOIL · WEATHER · PESTICIDE)  |  基于多因子 Cobb-Douglas 产量模型
+""", unsafe_allow_html=True) + +st.markdown("
", unsafe_allow_html=True) + +# KPI row +k1, k2, k3, k4 = st.columns(4) +with k1: + st.markdown(f""" +
+
{yph:,.0f}
+
kg / 公顷
+
{CROPS[selected_crop]['emoji']} {selected_crop} 单产
+
""", unsafe_allow_html=True) +with k2: + st.markdown(f""" +
+
{ytotal/1000:,.1f}
+
吨 / 总产量
+
📦 {area:.0f} 公顷总产
+
""", unsafe_allow_html=True) +with k3: + overall = np.mean(list(factors.values())) + st.markdown(f""" +
+
{overall*100:.1f}%
+
综合适宜度
+
🎯 环境匹配指数
+
""", unsafe_allow_html=True) +with k4: + st.markdown(f""" +
+
{best_crop['emoji']}
+
{best_crop['crop']} ({best_crop['score']*100:.0f}%)
+
🏆 最优推荐作物
+
""", unsafe_allow_html=True) + +st.markdown("
", unsafe_allow_html=True) + +# ─── Charts Row ────────────────────────────────────────────────────────────── +col_left, col_right = st.columns([3, 2]) + +with col_left: + st.markdown('
📊 影响因子雷达图
', unsafe_allow_html=True) + + 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,222,128,0.15)', + line=dict(color='#4ade80', 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(255,255,255,0.1)', 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='#64748b', size=9), + gridcolor='rgba(255,255,255,0.06)'), + angularaxis=dict(tickfont=dict(color='#e2e8f0', size=11), + gridcolor='rgba(255,255,255,0.08)'), + ), + paper_bgcolor='rgba(0,0,0,0)', + plot_bgcolor='rgba(0,0,0,0)', + font=dict(color='#e2e8f0'), + 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.markdown('
🏅 作物推荐排行
', unsafe_allow_html=True) + for i, r in enumerate(rankings[:4]): + rank_icons = ["🥇", "🥈", "🥉", "4️⃣"] + bar_width = int(r['score'] * 100) + bar_color = r['color'] + st.markdown(f""" +
+
+
+ {rank_icons[i]} + {r['emoji']} {r['crop']} +
+ {r['score']*100:.1f}% +
+
+
+
+
+ {r['yield_ha']:,.0f} kg/ha  ·  总产 {r['total_yield']/1000:,.1f} 吨 +
+
""", unsafe_allow_html=True) + +# ─── Sensitivity Analysis ───────────────────────────────────────────────────── +st.markdown('
📈 产量敏感性分析
', unsafe_allow_html=True) + +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='#4ade80', width=2.5), + fill='tozeroy', fillcolor='rgba(74,222,128,0.08)', + name='产量' + )) + fig_N.add_vline(x=N, line=dict(color='#f59e0b', width=1.5, dash='dot'), + annotation_text=f"当前 {N}", annotation_font_color='#f59e0b') + fig_N.update_layout( + title=dict(text="氮肥用量 vs 产量", font=dict(color='#94a3b8', size=12)), + xaxis=dict(title="氮 N (mg/kg)", color='#64748b', gridcolor='rgba(255,255,255,0.05)'), + yaxis=dict(title="产量 (kg/ha)", color='#64748b', gridcolor='rgba(255,255,255,0.05)'), + paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', + font=dict(color='#e2e8f0', 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='#38bdf8', width=2.5), + fill='tozeroy', fillcolor='rgba(56,189,248,0.08)', + name='产量' + )) + fig_R.add_vline(x=rainfall, line=dict(color='#f59e0b', width=1.5, dash='dot'), + annotation_text=f"当前 {rainfall}mm", annotation_font_color='#f59e0b') + fig_R.update_layout( + title=dict(text="月降雨量 vs 产量", font=dict(color='#94a3b8', size=12)), + xaxis=dict(title="降雨量 (mm/月)", color='#64748b', gridcolor='rgba(255,255,255,0.05)'), + yaxis=dict(title="产量 (kg/ha)", color='#64748b', gridcolor='rgba(255,255,255,0.05)'), + paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', + font=dict(color='#e2e8f0', 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.markdown('
🌐 全作物产量对比
', unsafe_allow_html=True) + +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(255,255,255,0.2)', width=1)), + text=[f"{y:,.0f}" for y in crop_yields], + textposition='outside', + textfont=dict(color='#94a3b8', size=10, family='JetBrains Mono'), +)) +fig_bar.update_layout( + xaxis=dict(color='#64748b', gridcolor='rgba(255,255,255,0.04)'), + yaxis=dict(title="预期产量 (kg/ha)", color='#64748b', gridcolor='rgba(255,255,255,0.05)'), + paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', + font=dict(color='#e2e8f0', size=11), + margin=dict(t=20, b=30, l=60, r=20), height=240, + showlegend=False, +) +st.plotly_chart(fig_bar, use_container_width=True) + +# ─── Advisory Panel ─────────────────────────────────────────────────────────── +st.markdown('
💡 智能建议
', unsafe_allow_html=True) +adv1, adv2 = st.columns(2) + +with adv1: + crop_opt = CROPS[selected_crop]["optimal"] + advisories = [] + + if not (crop_opt["ph"][0] <= ph <= crop_opt["ph"][1]): + advisories.append(("warn", f"pH {ph} 偏离 {selected_crop} 适宜范围 {crop_opt['ph']},建议{'施石灰' if ph < crop_opt['ph'][0] else '施硫磺'}调节")) + else: + advisories.append(("good", f"土壤 pH {ph} 处于 {selected_crop} 适宜范围内 ✓")) + + if N < crop_opt["N"][0]: + advisories.append(("warn", f"氮肥不足({N} vs 建议 {crop_opt['N'][0]}-{crop_opt['N'][1]} mg/kg),建议追施尿素")) + elif N > crop_opt["N"][1]: + advisories.append(("warn", f"氮肥过量({N} mg/kg),可能造成徒长,建议减施")) + else: + advisories.append(("good", f"氮肥水平 {N} mg/kg 适宜 ✓")) + + if rainfall < crop_opt["rainfall"][0]: + advisories.append(("warn", f"降雨量不足,建议增加灌溉(缺水 {crop_opt['rainfall'][0]-rainfall} mm)")) + elif rainfall > crop_opt["rainfall"][1]: + advisories.append(("warn", f"降雨量偏多,注意防涝排水")) + else: + advisories.append(("good", f"降雨量 {rainfall}mm 适合 {selected_crop} 生长 ✓")) + + for typ, msg in advisories: + css_class = "alert-good" if typ == "good" else "alert-warn" + st.markdown(f'
{msg}
', unsafe_allow_html=True) + +with adv2: + st.markdown(f""" +
+
+ 当前环境参数下适宜种植: +
+ """, unsafe_allow_html=True) + badges = "".join([ + f'{r["emoji"]} {r["crop"]} {r["score"]*100:.0f}%' + for r in rankings if r['score'] > 0.6 + ]) + st.markdown(f'{badges}
', unsafe_allow_html=True) + + st.markdown(f""" +
+ 最优方案:{best_crop['emoji']} {best_crop['crop']}
+ 预期单产:{best_crop['yield_ha']:,.0f} kg/ha
+ {area:.0f}公顷总产:{best_crop['total_yield']/1000:,.1f} 吨 +
+ """, unsafe_allow_html=True) + +# ─── Footer ─────────────────────────────────────────────────────────────────── +st.markdown("
", unsafe_allow_html=True) +st.markdown(""" +
+ YIELD = f(Soil, Weather, Pesticide)  |  Cobb-Douglas Multi-Factor Model  |  农业智能决策系统 +
+""", unsafe_allow_html=True) diff --git a/justfile b/justfile new file mode 100644 index 0000000..410315f --- /dev/null +++ b/justfile @@ -0,0 +1,30 @@ +# Justfile for YieldSmart 农业智能决策系统 +# Use `just ` to run tasks + +# Default task: show available commands +default: + just --list + +# Run the Streamlit app +run: + uv run streamlit run app.py + +# Install dependencies +install: + uv add streamlit ruff plotly pandas numpy + +# Format code with ruff +format: + uv run ruff format . + +# Check code with ruff +check: + uv run ruff check . + +# Run all checks and formatting +lint: + just format && just check + +# Clean up cache files +clean: + rm -rf __pycache__ .ruff_cache .streamlit_cache diff --git a/main.py b/main.py new file mode 100644 index 0000000..4e73d28 --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from YieldSmart!") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3c0452a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "yieldsmart" +version = "0.1.0" +description = "农业智能决策系统 - 基于多因子 Cobb-Douglas 产量模型的作物种植决策支持应用" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "numpy>=2.3.5", + "pandas>=2.3.3", + "plotly>=6.5.0", + "ruff>=0.14.8", + "streamlit>=1.52.1", +] + +[[tool.uv.index]] +url = "https://mirrors.aliyun.com/pypi/simple" +default = true