feat: 初始化 YieldSmart 农业智能决策系统
基于多因子 Cobb-Douglas 产量模型的作物种植决策支持应用。
新增文件:
- app.py: Streamlit 主应用,包含产量预测模型、多作物数据库、
雷达图/敏感性分析可视化、作物推荐排行及智能建议面板
- main.py: 入口文件
- pyproject.toml: 项目配置(Python 3.14+,依赖 streamlit/plotly/pandas/numpy)
- Dockerfile: 基于 uv 镜像的容器化部署配置
- justfile: 任务自动化(运行/格式化/检查/清理)
- .gitignore: Python/IDE/缓存忽略规则
This commit is contained in:
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -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/
|
||||||
43
Dockerfile
Normal file
43
Dockerfile
Normal file
@@ -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"]
|
||||||
90
README.md
90
README.md
@@ -1,3 +1,89 @@
|
|||||||
# yield-smart-app
|
# YieldSmart 农业智能决策系统
|
||||||
|
|
||||||
农业智能决策系统
|
基于多因子 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
|
||||||
|
|||||||
587
app.py
Normal file
587
app.py
Normal file
@@ -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("""
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-dark: #0a1628;
|
||||||
|
--bg-card: #0f2040;
|
||||||
|
--accent-green: #4ade80;
|
||||||
|
--accent-gold: #f59e0b;
|
||||||
|
--accent-blue: #38bdf8;
|
||||||
|
--text-primary: #e2e8f0;
|
||||||
|
--text-muted: #64748b;
|
||||||
|
--border: rgba(74, 222, 128, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, [class*="css"] {
|
||||||
|
font-family: 'Noto Serif SC', serif;
|
||||||
|
background-color: var(--bg-dark);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stApp {
|
||||||
|
background: linear-gradient(135deg, #0a1628 0%, #0d1f3c 50%, #091520 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
[data-testid="stSidebar"] {
|
||||||
|
background: linear-gradient(180deg, #0f2040 0%, #0a1628 100%);
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
[data-testid="stSidebar"] .stSlider label,
|
||||||
|
[data-testid="stSidebar"] .stNumberInput label,
|
||||||
|
[data-testid="stSidebar"] .stSelectbox label {
|
||||||
|
color: var(--accent-green) !important;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Metric cards */
|
||||||
|
.metric-card {
|
||||||
|
background: linear-gradient(135deg, #0f2040, #132b55);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.metric-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, var(--accent-green), var(--accent-blue));
|
||||||
|
}
|
||||||
|
.metric-value {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 2.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-green);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.metric-unit {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.metric-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section headers */
|
||||||
|
.section-header {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
color: var(--accent-gold);
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-bottom: 1px solid rgba(245,158,11,0.3);
|
||||||
|
padding-bottom: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Crop badge */
|
||||||
|
.crop-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: linear-gradient(135deg, rgba(74,222,128,0.15), rgba(56,189,248,0.15));
|
||||||
|
border: 1px solid var(--accent-green);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--accent-green);
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Recommendation card */
|
||||||
|
.rec-card {
|
||||||
|
background: linear-gradient(135deg, rgba(74,222,128,0.08), rgba(56,189,248,0.05));
|
||||||
|
border: 1px solid rgba(74,222,128,0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 18px 22px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.rec-rank {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--accent-gold);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.rec-crop {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.rec-score {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--accent-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero title */
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(135deg, var(--accent-green), var(--accent-blue));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.hero-sub {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert boxes */
|
||||||
|
.alert-good {
|
||||||
|
background: rgba(74,222,128,0.1);
|
||||||
|
border-left: 3px solid var(--accent-green);
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 8px 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.alert-warn {
|
||||||
|
background: rgba(245,158,11,0.1);
|
||||||
|
border-left: 3px solid var(--accent-gold);
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 8px 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override streamlit slider colors */
|
||||||
|
.stSlider [data-baseweb="slider"] [data-testid="stTickBarMin"],
|
||||||
|
.stSlider [data-baseweb="slider"] [data-testid="stTickBarMax"] {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
</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": "#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('<div class="hero-title">🌾 农业决策</div>', unsafe_allow_html=True)
|
||||||
|
st.markdown('<div class="hero-sub">SMART FARMING SYSTEM v2.0</div>', unsafe_allow_html=True)
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
st.markdown('<div class="section-header">🧪 土壤参数</div>', 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('<div class="section-header">🌦 气象数据</div>', 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('<div class="section-header">🌱 种植参数</div>', 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('<div class="section-header">🎯 目标作物</div>', 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"""
|
||||||
|
<div style="display:flex; align-items:baseline; gap:16px; margin-bottom:4px;">
|
||||||
|
<div class="hero-title">农业智能决策系统</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-sub">YIELD = f(SOIL · WEATHER · PESTICIDE) | 基于多因子 Cobb-Douglas 产量模型</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
st.markdown("<br>", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# KPI row
|
||||||
|
k1, k2, k3, k4 = st.columns(4)
|
||||||
|
with k1:
|
||||||
|
st.markdown(f"""
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-value">{yph:,.0f}</div>
|
||||||
|
<div class="metric-unit">kg / 公顷</div>
|
||||||
|
<div class="metric-label">{CROPS[selected_crop]['emoji']} {selected_crop} 单产</div>
|
||||||
|
</div>""", unsafe_allow_html=True)
|
||||||
|
with k2:
|
||||||
|
st.markdown(f"""
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-value">{ytotal/1000:,.1f}</div>
|
||||||
|
<div class="metric-unit">吨 / 总产量</div>
|
||||||
|
<div class="metric-label">📦 {area:.0f} 公顷总产</div>
|
||||||
|
</div>""", unsafe_allow_html=True)
|
||||||
|
with k3:
|
||||||
|
overall = np.mean(list(factors.values()))
|
||||||
|
st.markdown(f"""
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-value">{overall*100:.1f}%</div>
|
||||||
|
<div class="metric-unit">综合适宜度</div>
|
||||||
|
<div class="metric-label">🎯 环境匹配指数</div>
|
||||||
|
</div>""", unsafe_allow_html=True)
|
||||||
|
with k4:
|
||||||
|
st.markdown(f"""
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-value">{best_crop['emoji']}</div>
|
||||||
|
<div class="metric-unit">{best_crop['crop']} ({best_crop['score']*100:.0f}%)</div>
|
||||||
|
<div class="metric-label">🏆 最优推荐作物</div>
|
||||||
|
</div>""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
st.markdown("<br>", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# ─── Charts Row ──────────────────────────────────────────────────────────────
|
||||||
|
col_left, col_right = st.columns([3, 2])
|
||||||
|
|
||||||
|
with col_left:
|
||||||
|
st.markdown('<div class="section-header">📊 影响因子雷达图</div>', 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('<div class="section-header">🏅 作物推荐排行</div>', 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"""
|
||||||
|
<div class="rec-card" style="margin-bottom:8px;">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||||
|
<div>
|
||||||
|
<span class="rec-rank">{rank_icons[i]}</span>
|
||||||
|
<span class="rec-crop" style="margin-left:8px;">{r['emoji']} {r['crop']}</span>
|
||||||
|
</div>
|
||||||
|
<span class="rec-score">{r['score']*100:.1f}%</span>
|
||||||
|
</div>
|
||||||
|
<div style="background:rgba(255,255,255,0.06); border-radius:4px; height:4px; margin-top:8px; overflow:hidden;">
|
||||||
|
<div style="width:{bar_width}%; height:100%; background:{bar_color}; border-radius:4px;"></div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:0.78rem; color:#64748b; margin-top:4px; font-family:'JetBrains Mono',monospace;">
|
||||||
|
{r['yield_ha']:,.0f} kg/ha · 总产 {r['total_yield']/1000:,.1f} 吨
|
||||||
|
</div>
|
||||||
|
</div>""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# ─── Sensitivity Analysis ─────────────────────────────────────────────────────
|
||||||
|
st.markdown('<div class="section-header">📈 产量敏感性分析</div>', 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('<div class="section-header">🌐 全作物产量对比</div>', 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('<div class="section-header">💡 智能建议</div>', 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'<div class="{css_class}">{msg}</div>', unsafe_allow_html=True)
|
||||||
|
|
||||||
|
with adv2:
|
||||||
|
st.markdown(f"""
|
||||||
|
<div class="rec-card">
|
||||||
|
<div style="font-size:0.82rem; color:#64748b; font-family:'JetBrains Mono',monospace; margin-bottom:12px;">
|
||||||
|
当前环境参数下适宜种植:
|
||||||
|
</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
badges = "".join([
|
||||||
|
f'<span class="crop-badge">{r["emoji"]} {r["crop"]} {r["score"]*100:.0f}%</span>'
|
||||||
|
for r in rankings if r['score'] > 0.6
|
||||||
|
])
|
||||||
|
st.markdown(f'{badges}</div>', unsafe_allow_html=True)
|
||||||
|
|
||||||
|
st.markdown(f"""
|
||||||
|
<div style="margin-top:16px; font-size:0.85rem; color:#94a3b8; line-height:1.7;">
|
||||||
|
<b style="color:#4ade80;">最优方案:</b>{best_crop['emoji']} {best_crop['crop']}<br>
|
||||||
|
预期单产:<span style="font-family:'JetBrains Mono',monospace; color:#38bdf8;">{best_crop['yield_ha']:,.0f} kg/ha</span><br>
|
||||||
|
{area:.0f}公顷总产:<span style="font-family:'JetBrains Mono',monospace; color:#38bdf8;">{best_crop['total_yield']/1000:,.1f} 吨</span>
|
||||||
|
</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# ─── Footer ───────────────────────────────────────────────────────────────────
|
||||||
|
st.markdown("<br>", unsafe_allow_html=True)
|
||||||
|
st.markdown("""
|
||||||
|
<div style="text-align:center; font-family:'JetBrains Mono',monospace; font-size:0.72rem;
|
||||||
|
color:#334155; padding:16px; border-top:1px solid rgba(74,222,128,0.1);">
|
||||||
|
YIELD = f(Soil, Weather, Pesticide) | Cobb-Douglas Multi-Factor Model | 农业智能决策系统
|
||||||
|
</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
30
justfile
Normal file
30
justfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Justfile for YieldSmart 农业智能决策系统
|
||||||
|
# Use `just <command>` 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
|
||||||
6
main.py
Normal file
6
main.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
def main():
|
||||||
|
print("Hello from YieldSmart!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
17
pyproject.toml
Normal file
17
pyproject.toml
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user