import { useMemo, useState } from "react"; import type { ResearchSnapshot } from "@/lib/ai-research"; import { groupIncome, platformIncomeSample } from "@/lib/research-statistics"; const usd = (value: number | null) => value === null ? "—" : new Intl.NumberFormat("zh-CN", { style: "currency", currency: "USD", maximumFractionDigits: 0, }).format(value); const percent = (value: number | null) => value === null ? "—" : `${(value * 100).toFixed(1)}%`; export default function IncomeDistribution({ data, onSelect }: { data: ResearchSnapshot; onSelect: (id: string) => void; }) { const [dateWindow, setDateWindow] = useState("7"); const [year, setYear] = useState(""); const [annualize, setAnnualize] = useState(false); const [splitModels, setSplitModels] = useState(true); const [logScale, setLogScale] = useState(true); const sample = useMemo(() => platformIncomeSample(data, { windowDays: dateWindow === "all" ? null : Number(dateWindow), foundedYear: year ? Number(year) : undefined, annualize, }), [data, dateWindow, year, annualize]); const groups = useMemo(() => groupIncome(sample.points, splitModels).sort((a, b) => a.category.localeCompare(b.category) || a.provider.localeCompare(b.provider) || a.model.localeCompare(b.model)), [sample.points, splitModels]); const categories = new Map(data.categories.map((category) => [category.id, category.name])); const metric = annualize ? "MRR×12参考年化" : "平台MRR"; const values = sample.points.map((point) => point.value); const minValue = Math.min(...values); const maxValue = Math.max(1, ...values); const axisMin = logScale && minValue > 0 && Number.isFinite(minValue) ? 10 ** Math.floor(Math.log10(minValue)) : 0; const axisMax = Math.max(axisMin * 10, 10 ** Math.ceil(Math.log10(maxValue))); const scale = logScale ? (value: number) => Math.log10(value + 1) : (value: number) => value; const left = 250, right = 865; const x = (value: number) => left + (scale(value) - scale(axisMin)) / (scale(axisMax) - scale(axisMin)) * (right - left); const ticks = logScale ? [0, 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000] .filter((value) => value >= axisMin && value <= axisMax) : [0, .25, .5, .75, 1].map((fraction) => fraction * axisMax); return

收入分布与均值敏感性

单独比较支付平台显示的 USD MRR。每个点是一个产品或业务,法律主体与支付账户的独立性尚未全部核实;这不是行业总体或公司财报样本。

资料截至 {data.asOf},纳入 {sample.points.length} 个产品,另有 {sample.excluded.length} 个 MRR 观测未纳入。{annualize ? "乘12只改变显示单位,不证明全年收入、续约或ARR;CV与相对敏感性不变。" : "MRR不等于近30日收款;平台数据连接也不等于财务审计。"}{dateWindow === "all" && " 当前包含不同时间的历史观测,不能称为同期对照。"}

{sample.points.length ? <>
{ticks.map((tick) => {usd(tick)})} {groups.map((group, index) => { const y = index * 70 + 40; return {categories.get(group.category)} {group.provider} · {group.model} · n={group.stats.n} 样本均值 {usd(group.stats.mean)} 样本中位数 {usd(group.stats.median)} {group.points.map((point, position) => onSelect(point.company.id)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onSelect(point.company.id); } }}> {point.company.name} · {usd(point.value)} · 数据日期 {point.observation.observedAt}{point.observation.connectionExpired ? " · 连接提示过期" : ""} )} ; })}
圆点:产品观测 竖线:样本均值 菱形:样本中位数。按业务类别与支付来源分组,点击点或下表产品名可看原始证据。
{groups.map((group) => )}
{metric}样本统计;金额按显示值计算,CV使用样本标准差
类别 / 来源 / 模式n均值中位数均值/中位数CV去除最大值后的均值 / 下降幅度产品与限制
{categories.get(group.category)}{group.provider} · {group.model}{group.stats.n}{usd(group.stats.mean)}{usd(group.stats.median)}{group.stats.meanMedianRatio?.toFixed(2) ?? "—"}{group.stats.cv?.toFixed(2) ?? "—"}{usd(group.stats.meanWithoutLargest)}{percent(group.stats.largestRemovalDrop)} {group.points.map((point) => {" "})}{group.stats.n < 3 ? "样本少于3,不能据此判断领域典型收入。" : "自选的小样本,须结合产品年龄、成本与留存评估。"}
:

当前窗口和创立年份没有符合条件的 MRR 记录。

}
统计规则与解释

每个产品只取符合条件的最新一条平台 MRR;币种为美元,要求有效数据日期、确定数值及支付来源。公司 ARR、估算、融资、目标和近30日收入均不进入这里。未知值不补零。

CV=样本标准差÷均值,标准差分母为 n−1;n=1 不算 CV。n=2 时均值必然等于中位数,不能据此认为均值有代表性。较大的均值/中位数差距、CV或移除最大值的变化,仅提示进一步核查样本构成。

支付商与平台的退款、试用、年付、币种及指标计算可能不同(平台方法说明)。不能将 MRR 与收款差异、单个高值或高离散度直接解释为财务造假。比较商业模式还需要毛利、获客成本、留存、人工交付和客户集中度。

{sample.excluded.length > 0 &&
未纳入的 MRR 记录({sample.excluded.length})
}
; }