You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
hy-screen-2.0/scripts/prepare-artificial-grasslan...

278 lines
10 KiB

#!/usr/bin/env python3
"""Build the artificial-grassland business summary used by the big screen.
Usage:
python3 scripts/prepare-artificial-grassland-business-data.py <source.xlsx>
The source workbook contains merged project rows. This script keeps the
workbook's total row as the reporting baseline and only uses the detail rows
to identify project coverage towns.
"""
from __future__ import annotations
import json
import re
import sys
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
from openpyxl import load_workbook
ROOT = Path(__file__).resolve().parents[1]
TARGET = ROOT / "src/views/yakIndustryChain/artificialGrasslandBusinessSummary.json"
TOTAL_ROW_LABEL = "合计面积"
TOWNS = [
("邛溪镇", ("邛溪",)),
("刷经寺镇", ("刷经寺",)),
("安曲镇", ("安曲",)),
("龙日镇", ("龙日",)),
("江茸乡", ("江茸",)),
("查尔玛乡", ("查尔玛",)),
("瓦切镇", ("瓦切",)),
("阿木乡", ("阿木",)),
("麦洼乡", ("麦洼",)),
("色地镇", ("色地",)),
]
def text(value: object) -> str:
return str(value or "").replace("\n", " ").strip()
def number(value: object) -> float:
try:
return float(value)
except (TypeError, ValueError):
return 0.0
def display_number(value: float) -> int | float:
rounded = round(value, 2)
return int(rounded) if rounded.is_integer() else rounded
def first_text(ws, rows, column: int) -> str:
for row in rows:
value = text(ws.cell(row, column).value)
if value:
return value
return ""
def first_number(ws, rows, column: int) -> float:
for row in rows:
value = number(ws.cell(row, column).value)
if value:
return value
return 0.0
def sum_numbers(ws, rows, column: int) -> float:
return sum(number(ws.cell(row, column).value) for row in rows)
def project_year(name: str, notes: list[str]) -> str:
year_match = re.search(r"20(2[0-9])", name)
if year_match:
return f"20{year_match.group(1)}"
note_text = " ".join(notes)
year_match = re.search(r"20(2[0-9])", note_text)
if year_match:
return f"20{year_match.group(1)}"
short_match = re.search(r"(?<!\d)(2[0-9])年", note_text)
if short_match:
return f"20{short_match.group(1)}"
return "待核定"
def project_type(name: str) -> str:
if "草种基地" in name:
return "草种基地"
if "草地资源保护" in name:
return "草地资源保护"
return "高产稳产饲草基地"
def project_short_name(name: str, year: str, category: str) -> str:
if year in {"2022", "2023", "2024"}:
if category == "高产稳产饲草基地":
return f"{year}饲草基地"
return f"{year}{category}"
return category
def contract_area(requirement: str) -> float:
values = [number(item) for item in re.findall(r"(\d+(?:\.\d+)?)\s*亩", requirement)]
return sum(values)
def locate_towns(locations: list[str]) -> list[str]:
found = []
for location in locations:
# A location can mention a direction or a park in another town. Prefer
# the explicit town name first, then use a prefix only when no town is
# written in the location itself.
explicit = []
for town, aliases in TOWNS:
if any(f"{alias}" in location or f"{alias}" in location for alias in aliases):
explicit.append(town)
candidates = explicit
if not candidates:
normalized = location.lstrip("(( ")
candidates = [
town
for town, aliases in TOWNS
if any(normalized.startswith(alias) for alias in aliases)
]
for town in candidates:
if town not in found:
found.append(town)
return found
def make_row(key: str, name: str, value: float, unit: str, extra: str = "") -> dict:
return {
"key": key,
"name": name,
"value": display_number(value),
"unit": unit,
"extra": extra,
}
def main() -> None:
if len(sys.argv) < 2:
raise SystemExit("Usage: python3 scripts/prepare-artificial-grassland-business-data.py <source.xlsx>")
source = Path(sys.argv[1]).expanduser().resolve()
workbook = load_workbook(source, data_only=True)
sheet = workbook.active
total_row = next(
(
row
for row in range(2, sheet.max_row + 1)
if TOTAL_ROW_LABEL in text(sheet.cell(row, 1).value)
),
None,
)
if not total_row:
raise RuntimeError("Cannot find the workbook total row.")
starts = [
row
for row in range(2, total_row)
if text(sheet.cell(row, 1).value)
]
projects = []
for index, start in enumerate(starts):
end = starts[index + 1] - 1 if index + 1 < len(starts) else total_row - 1
rows = list(range(start, end + 1))
name = text(sheet.cell(start, 1).value)
requirement = first_text(sheet, rows, 5)
locations = [text(sheet.cell(row, 6).value) for row in rows if text(sheet.cell(row, 6).value)]
notes = [text(sheet.cell(row, 13).value) for row in rows if text(sheet.cell(row, 13).value)]
year = project_year(name, notes)
category = project_type(name)
actual_area = sum_numbers(sheet, rows, 7)
contract = contract_area(requirement)
project = {
"key": f"artificial-grassland-project-{index + 1}",
"name": project_short_name(name, year, category),
"fullName": name,
"year": year,
"category": category,
"value": display_number(actual_area),
"unit": "",
"actualAreaMu": display_number(actual_area),
"contractAreaMu": display_number(contract),
"investmentWanYuan": display_number(first_number(sheet, rows, 4)),
"owner": first_text(sheet, rows, 2),
"contractor": first_text(sheet, rows, 3),
"requirement": requirement,
"towns": locate_towns(locations),
"locations": locations,
"notes": notes,
"canLocate": False,
"searchText": " ".join([name, year, category, *locations, *notes]),
}
projects.append(project)
actual_total = number(sheet.cell(total_row, 7).value)
contract_total = sum(number(project["contractAreaMu"]) for project in projects)
investment_total = number(sheet.cell(total_row, 4).value)
town_project_count = defaultdict(int)
for project in projects:
for town in project["towns"]:
town_project_count[town] += 1
year_area = defaultdict(float)
type_area = defaultdict(float)
for project in projects:
if project["year"] in {"2022", "2023", "2024"}:
year_area[project["year"]] += number(project["actualAreaMu"])
type_area[project["category"]] += number(project["actualAreaMu"])
investment_rows = [
make_row(project["key"], project["name"], number(project["investmentWanYuan"]), "万元", project["year"])
for project in projects
if number(project["investmentWanYuan"]) > 0
]
investment_rows.sort(key=lambda item: number(item["value"]), reverse=True)
summary = {
"generatedAt": datetime.now(timezone.utc).isoformat(),
"sourceLabel": source.name,
"sourceSheet": sheet.title,
"sourceNote": "按汇总表合计行作为大屏面积口径;图斑资料仅用于地图定位,不作为统计主口径。",
"summary": {
"actualAreaMu": display_number(actual_total),
"contractAreaMu": display_number(contract_total),
"areaDifferenceMu": display_number(actual_total - contract_total),
"projectCount": len(projects),
"coveredTownCount": len(town_project_count),
"constructionYearCount": len(year_area),
"investmentRecordCount": len(investment_rows),
"investmentWanYuan": display_number(investment_total),
"tillageDisturbanceMu": display_number(number(sheet.cell(total_row, 8).value)),
"slightDisturbanceMu": display_number(number(sheet.cell(total_row, 9).value)),
"tillageMu": display_number(number(sheet.cell(total_row, 10).value)),
"drillSowingMu": display_number(number(sheet.cell(total_row, 11).value)),
"noTillOverseedingMu": display_number(number(sheet.cell(total_row, 12).value)),
},
"projectRows": projects,
"yearRows": [
make_row(f"year-{year}", f"{year}", year_area[year], "", "实测种草面积")
for year in sorted(year_area)
],
"typeRows": [
make_row(f"type-{category}", category, value, "", "实测种草面积")
for category, value in sorted(type_area.items(), key=lambda item: item[1], reverse=True)
],
"operationRows": [
make_row("tillage", "实际翻耕", number(sheet.cell(total_row, 10).value), "", "表内实测记录"),
make_row("drill-sowing", "条播", number(sheet.cell(total_row, 11).value), "", "表内实测记录"),
make_row("no-till-overseeding", "免耕补播", number(sheet.cell(total_row, 12).value), "", "表内实测记录"),
],
"disturbanceRows": [
make_row("disturbance-total", "翻耕及扰动", number(sheet.cell(total_row, 8).value), "", "施工扰动面积"),
make_row("disturbance-light", "轻微扰动", number(sheet.cell(total_row, 9).value), "", "施工扰动面积"),
make_row("disturbance-tillage", "实际翻耕", number(sheet.cell(total_row, 10).value), "", "施工扰动面积"),
],
"townRows": [
make_row(f"town-{town}", town, town_project_count[town], "", "涉及种草项目")
for town in town_project_count
],
"investmentRows": investment_rows,
}
TARGET.parent.mkdir(parents=True, exist_ok=True)
TARGET.write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(f"Generated {TARGET}")
print(json.dumps(summary["summary"], ensure_ascii=False))
if __name__ == "__main__":
main()