Spaces:
Running
Running
Download batch_updates_dashboard.py from AutoZYX/nds-statistics-platform: direct link, hf CLI and curl.
- Browser
- Download file 11.7 kB
-
https://huggingface.co/spaces/AutoZYX/nds-statistics-platform/resolve/main/batch_updates_dashboard.py
- Command line
-
hf download hf://spaces/AutoZYX/nds-statistics-platform/batch_updates_dashboard.py
-
curl -L -o batch_updates_dashboard.py https://huggingface.co/spaces/AutoZYX/nds-statistics-platform/resolve/main/batch_updates_dashboard.py
11.7 kB
| """All active collection batches and database ingestion updates.""" | |
| from __future__ import annotations | |
| from collections import Counter | |
| from datetime import date, datetime, timedelta | |
| import html | |
| import math | |
| from typing import Callable, Iterable | |
| from zoneinfo import ZoneInfo | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| import streamlit as st | |
| from batch_updates import BatchUpdateCatalog, BatchUpdateRecord | |
| from city_labels import city_display_name | |
| from dashboard_charts import DATA_BLUE, style_figure | |
| _BEIJING = ZoneInfo('Asia/Shanghai') | |
| def ingestion_totals(catalog: BatchUpdateCatalog, today: date) -> tuple[int, int, int]: | |
| """Today and inclusive last 7/30 Beijing days, excluding future entries.""" | |
| counts = Counter() | |
| for record in catalog.records: | |
| for item in record.ingestion_days: | |
| counts[item.day] += item.videos | |
| return tuple(sum(count for day, count in counts.items() | |
| if today - timedelta(days=window-1) <= day <= today) | |
| for window in (1, 7, 30)) | |
| def ingestion_timeline(catalog: BatchUpdateCatalog, today: date, | |
| window: int | None = 30) -> list[tuple[date, int]]: | |
| """Fill Beijing calendar days through today; future rows never add activity.""" | |
| counts = Counter() | |
| for record in catalog.records: | |
| for item in record.ingestion_days: | |
| if item.day <= today: | |
| counts[item.day] += item.videos | |
| if window is None: | |
| if not counts: | |
| return [] | |
| start = min(counts) | |
| else: | |
| start = today - timedelta(days=window-1) | |
| return [(start + timedelta(days=offset), counts[start + timedelta(days=offset)]) | |
| for offset in range((today-start).days+1)] | |
| def _hour_days(catalog: BatchUpdateCatalog, today: date) -> dict[date, tuple[float, int, int]] | None: | |
| if not catalog.includes_durations: | |
| return None | |
| buckets = {} | |
| for record in catalog.records: | |
| for item in record.ingestion_days: | |
| if item.day <= today: | |
| bucket = buckets.setdefault(item.day, [[], 0, 0]) | |
| bucket[0].append(item.known_seconds) | |
| bucket[1] += item.known_videos | |
| bucket[2] += item.unknown_videos | |
| return {day: (math.fsum(values[0]), values[1], values[2]) for day, values in buckets.items()} | |
| def ingestion_hours(catalog: BatchUpdateCatalog, today: date) -> tuple[float | None, ...]: | |
| """Known active-video hours received in Beijing day windows, never frame estimates.""" | |
| days = _hour_days(catalog, today) | |
| if days is None: | |
| return None, None, None | |
| totals = [] | |
| for window in (1, 7, 30): | |
| selected = [values for day, values in days.items() | |
| if today - timedelta(days=window-1) <= day <= today] | |
| known, unknown = sum(row[1] for row in selected), sum(row[2] for row in selected) | |
| totals.append(math.fsum(row[0] for row in selected) / 3600 if known or not unknown else None) | |
| return tuple(totals) | |
| def ingestion_hour_timeline(catalog: BatchUpdateCatalog, today: date, | |
| window: int | None = 30) -> list[tuple[date, float | None]] | None: | |
| """No arrivals is measured zero; all-unknown arrivals leave an explicit gap.""" | |
| days = _hour_days(catalog, today) | |
| if days is None: | |
| return None | |
| if window is None and not days: | |
| return [] | |
| start = min(days) if window is None else today - timedelta(days=window-1) | |
| rows = [] | |
| for offset in range((today-start).days+1): | |
| day = start + timedelta(days=offset) | |
| seconds, known, unknown = days.get(day, (0, 0, 0)) | |
| rows.append((day, seconds / 3600 if known or not unknown else None)) | |
| return rows | |
| def batch_table(records: Iterable[BatchUpdateRecord], translate: Callable) -> pd.DataFrame: | |
| """Export only curated batch fields, with public timestamps in Beijing time.""" | |
| def stamp(value): | |
| return value.astimezone(_BEIJING).strftime('%Y-%m-%d %H:%M:%S') if value else translate('未知') | |
| records = sorted(records, key=lambda record: (record.last_ingested_at is None, | |
| -record.last_ingested_at.timestamp() if record.last_ingested_at else 0, record.code)) | |
| columns = tuple(map(translate, ('批次编号','城市','已知时长 (h)','首次入库','最近入库', | |
| '源数据更新','标注完成','可用标签'))) | |
| return pd.DataFrame([ | |
| (record.code, city_display_name(record.city), record.duration_hours, stamp(record.first_ingested_at), | |
| stamp(record.last_ingested_at), stamp(record.source_updated_at), | |
| stamp(record.annotation_completed_at), translate('有' if record.annotations_available else '暂无')) | |
| for record in records | |
| ], columns=columns) | |
| def render_batch_updates(load_updates: Callable, translate: Callable) -> None: | |
| def tr(zh, en): | |
| translated = translate(zh) | |
| return en if st.session_state.get('lang') == 'en' and translated == zh else translated | |
| names = {'批次编号':'Batch code','城市':'City','已知时长 (h)':'Known duration (h)', | |
| '首次入库':'First ingested','最近入库':'Last ingested','源数据更新':'Source updated', | |
| '标注完成':'Annotation completed','可用标签':'Usable labels','有':'Available', | |
| '暂无':'Unavailable','未知':'Unknown'} | |
| def table_translate(value): | |
| return tr(value, names.get(value, value)) | |
| st.markdown("<div id='nav-updates' class='dash-section'><h2 class='dash-section-title'>" | |
| + html.escape(tr('数据更新', 'Dataset updates')) + '</h2></div>', unsafe_allow_html=True) | |
| st.caption(tr( | |
| '这里展示全部活跃采集批次,独立于城市、采集日期与 ODD 筛选。入库时间是数据库接收视频的时间,' | |
| '不等于相机采集时间;新增量按当前活跃视频的首次入库日计算,不是包含删除记录的历史审计。' | |
| '时间统一为北京时间;标注完成取实际处理结束时间,源数据更新取视频修改时间。', | |
| 'All active collection batches are shown independently of city, collection-date and ODD filters. ' | |
| 'Ingestion means database receipt, not camera collection. Additions use the creation dates of currently ' | |
| 'active videos; this is not a deletion audit. Times use Beijing time. Annotation completion uses the ' | |
| 'actual processing finish time; source updates use video modification time.')) | |
| catalog, status = load_updates() | |
| if status != 'ok' or catalog is None: | |
| st.warning(tr('数据更新暂不可用,请稍后刷新。', 'Dataset updates are unavailable. Please refresh later.')) | |
| return | |
| today = datetime.now(_BEIJING).date() | |
| today_hours, seven_hours, month_hours = ingestion_hours(catalog, today) | |
| with st.container(key='batch_updates_kpis'): | |
| for column, label, english, value in zip(st.columns(4), | |
| ('活跃采集批次','今日新增 (h)','近 7 日新增 (h)','近 30 日新增 (h)'), | |
| ('Active batches','Added today (h)','Added in 7 days (h)','Added in 30 days (h)'), | |
| (len(catalog.records), today_hours, seven_hours, month_hours)): | |
| column.metric(tr(label, english), (f'{value:,}' if label == '活跃采集批次' | |
| else f'{value:,.3f}' if value is not None else '—')) | |
| if not catalog.includes_durations: | |
| st.info(tr('批次及每日入库时长尚未发布;暂不显示时长趋势,不以视频数量换算。', | |
| 'Batch and daily ingestion durations have not been published. The duration trend is unavailable.')) | |
| elif any(record.unknown_videos for record in catalog.records): | |
| st.caption(tr('含未知时长数据,仅合计实际已知时长;全未知显示空值,测得零时长保留为 0 h。', | |
| 'Some durations are unknown. Only measured duration is summed; all-unknown values are blank, measured zero remains 0 h.')) | |
| if any(sum(day.videos for day in record.ingestion_days) < record.videos for record in catalog.records): | |
| st.caption(tr('存在入库日期缺失的数据;每日趋势和近期新增指标仅统计有入库日期的已知时长。', | |
| 'Some ingestion dates are missing. Daily and recent additions include only measured duration with an ingestion date.')) | |
| if catalog.unlinked_videos: | |
| hours = catalog.unlinked_duration_hours | |
| value = f'{hours:,.3f} h' if hours is not None else tr('时长未知或尚未发布', 'duration unknown or unpublished') | |
| st.info(tr(f'另有未关联采集批次的数据(已知部分:{value}),未计入批次新增量与下表。', | |
| f'Unlinked data (known portion: {value}) is excluded from batch additions and the table.')) | |
| if not catalog.records: | |
| st.info(tr('暂无活跃采集批次。', 'No active collection batches are available.')) | |
| return | |
| ranges = (tr('近 7 日', 'Last 7 days'), tr('近 30 日', 'Last 30 days'), tr('全部', 'All time')) | |
| selected_range = st.selectbox(tr('入库时间范围', 'Ingestion time range'), ranges, index=1, | |
| key='batch_updates_range') | |
| window = {ranges[0]:7, ranges[1]:30, ranges[2]:None}[selected_range] | |
| timeline = ingestion_hour_timeline(catalog, today, window) | |
| if timeline: | |
| figure = go.Figure(go.Bar(x=[day.isoformat() for day, _ in timeline], | |
| y=[count for _, count in timeline], marker_color=DATA_BLUE, | |
| hovertemplate='%{x}: %{y:.3f} h<extra></extra>')) | |
| style_figure(figure, title=tr('每日新增时长 (h) · 当前活跃数据', 'Daily ingested duration (h) · currently active data'), | |
| height=300) | |
| figure.update_xaxes(title=tr('首次入库日(北京时间)', 'First ingestion day (Beijing)'), type='date') | |
| figure.update_yaxes(title='h', rangemode='tozero', tickformat='.3~f') | |
| st.plotly_chart(figure, width='stretch', key='batch_updates_timeline') | |
| elif timeline is not None: | |
| st.info(tr('当前批次尚无可用入库日期。', 'No ingestion dates are available for these batches.')) | |
| left, right = st.columns(2) | |
| query = left.text_input(tr('搜索批次编号或城市', 'Search batch code or city'), key='batch_updates_search').strip().casefold() | |
| cities = right.multiselect(tr('更新表城市', 'Batch table cities'), sorted({r.city for r in catalog.records}), | |
| key='batch_updates_cities', format_func=city_display_name) | |
| selected = tuple(record for record in catalog.records | |
| if (not cities or record.city in cities) | |
| and (not query or query in record.code.casefold() or query in record.city.casefold() | |
| or query in city_display_name(record.city).casefold())) | |
| st.caption(tr(f'显示 {len(selected):,} / {len(catalog.records):,} 个批次;CSV 始终包含全部活跃批次。', | |
| f'Showing {len(selected):,} / {len(catalog.records):,} batches; CSV always includes all active batches.')) | |
| st.dataframe(batch_table(selected, table_translate), hide_index=True, width='stretch') | |
| st.download_button(tr('下载全部批次 CSV', 'Download all batches CSV'), | |
| batch_table(catalog.records, table_translate).to_csv(index=False).encode('utf-8-sig'), | |
| file_name='aerial-batch-updates.csv', mime='text/csv', key='batch_updates_csv') | |