新增支付平台收入样本与分布分析
This commit is contained in:
@@ -98,13 +98,26 @@ AUTH_CONFIG_FILE=/private/tmp/tabularium-auth/config.json AUTH_COOKIE_SECURE=fal
|
||||
同一版本内容变化会拒绝导入,避免静默读取旧数据。回退代码时继续读取对应旧版本。
|
||||
所有修改都经过工作分支和 PR,部署仍等待人工合并主线。
|
||||
|
||||
当前资料包含 986 条公司记录、10 类业务及待分类组、1,035 条证据记录和 75 条经营观测。
|
||||
其中保留 58 家个案资料,新增 928 条目录基础资料;目录项不等于完成财务和技术核验。
|
||||
当前资料包含 1,002 条公司/产品记录、10 类业务及待分类组、1,055 条证据记录和 126 条经营观测。
|
||||
其中保留 58 家个案资料、928 条目录基础资料,并新增 16 个平台产品/业务样本;目录项不等于完成财务和技术核验。
|
||||
它是定向试样,收入披露不足且统计时点不统一,不代表全部 AI 公司。
|
||||
界面将公司自述、第三方估算、融资、客户收入和履历收入分开,未知收入不记为零。
|
||||
保留被撤回的声明作为核查记录,并排除收入比较;并购后业务、管理层预测与历史里程碑单列。
|
||||
团队规模下界在列表中显示比较符,商业成熟度展示公开交付证据,不等于技术能力排名。
|
||||
当前尚不能可靠计算领域 ARR 均值、离散排名或财务造假概率。
|
||||
公司披露的 ARR 尚未建立同期同口径比较组。新增平台 MRR 单独计算探索性样本统计,
|
||||
不能称行业平均 ARR、总体离散排名或财务造假概率。
|
||||
|
||||
收入分布:`IncomeDistribution.tsx` 从数据库返回的观测中筛选平台 USD MRR,
|
||||
每个产品只保留符合条件的最新值,默认数据日期距截止日不超过7天。
|
||||
按业务类别、支付来源及可选收费模式分组,显示均值、中位数、样本CV(标准差分母n−1)、
|
||||
均值/中位数和移除最大值后的均值敏感性。n<3标注样本不足;n=1不计算CV。
|
||||
MRR×12仅作为显示单位转换,不改写为公司ARR;公司ARR、估算与近30日收入均不混入。
|
||||
`ai-income-analysis.json` 是独立Python复算的7日/90日快照参考,用于检查统计选择和计算,
|
||||
页面本身从同一SQLite中的公司与观测记录计算,不读取第二个数据库。
|
||||
|
||||
平台同步时间、连接过期状态、业务描述的可编辑性和产品/法律主体未去重限制均保留。
|
||||
默认7日样本14个产品;90日窗口新增Gojiberry的8月历史点,营销样本的均值和CV随之明显变化。
|
||||
这提示先核查时间与样本构成,不能将高离散度直接解释为造假。两个近名营养应用按不同App Store ID分别建档。
|
||||
|
||||
覆盖核查:从 yc-oss 非官方镜像发现 2025—2026 八个 YC 批次的 1,428 条候选,
|
||||
逐条读取对应 YC 官方公开页面,以页面中的成立年份、团队字段和业务描述筛选。
|
||||
@@ -126,6 +139,8 @@ AI 关键词可能出现在业务背景中,收入、买家和交付状态仍
|
||||
## CI/CD 与缓存
|
||||
|
||||
`.gitea/workflows/ci-deploy.yml` 使用已有 ces-control Runner,不修改共享 Runner 配置。
|
||||
当前工作流仅在 main 推送或针对 main 的手动运行时执行。工作分支PR完成本地静态核查后,
|
||||
完整类型、测试、构建与容器检查在用户手动合并后的主线CI运行;报告须区分未运行与已通过。
|
||||
|
||||
1. 对实际构建输入计算 SHA-256:前端源码、Markdown、图片、Python 制图源码、依赖锁文件、Dockerfile 和构建脚本。
|
||||
2. 命中 `verified-<scope>-<摘要>` 镜像时跳过依赖安装和 Next.js build,只做认证、实际路由与资源冒烟检查。
|
||||
|
||||
@@ -185,10 +185,10 @@ async function main() {
|
||||
assert.equal(researchResponse.status, 200, "authenticated research API");
|
||||
assert.ok(researchResponse.headers.get("cache-control")?.includes("no-store"));
|
||||
const research = await researchResponse.json();
|
||||
assert.equal(research.companies.length, 986);
|
||||
assert.equal(research.companies.length, 1002);
|
||||
assert.equal(research.categories.length, 11);
|
||||
assert.equal(research.evidence.length, 1035);
|
||||
assert.equal(research.observations.length, 75);
|
||||
assert.equal(research.evidence.length, 1055);
|
||||
assert.equal(research.observations.length, 126);
|
||||
assert.equal((await request("/api/research/ai-companies", { method: "POST", headers })).status, 405);
|
||||
const researchPage = await request("/docs/other-research/ai-company-research/overview", { headers });
|
||||
assert.equal(researchPage.status, 200);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import IncomeDistribution from "./IncomeDistribution";
|
||||
import {
|
||||
categorySummary, evidenceUrl, type ResearchEvidence, type ResearchSnapshot,
|
||||
} from "@/lib/ai-research";
|
||||
|
||||
const pageSize = 12;
|
||||
const evidencePageSize = 30;
|
||||
const revenueLabels = { self: "公司披露,待核口径", estimate: "仅第三方估算", unknown: "暂无可用收入数字" };
|
||||
const revenueLabels = { self: "公司披露,待核口径", estimate: "仅第三方估算", platform: "仅支付平台记录", unknown: "暂无可用收入数字" };
|
||||
const useLabels: Record<string, string> = {
|
||||
signal: "公司披露信号", estimate: "第三方估算", historical: "历史里程碑",
|
||||
platform: "支付平台记录", signal: "公司披露信号", estimate: "第三方估算", historical: "历史里程碑",
|
||||
"historical-estimate": "其他时点估算", excluded: "排除收入比较", scale: "团队规模", adoption: "采用规模",
|
||||
funding: "融资", crosscheck: "同一里程碑的其他资料",
|
||||
};
|
||||
@@ -70,7 +71,8 @@ export default function AICompanyResearch() {
|
||||
const categories = new Map(data.categories.map((c) => [c.id, c]));
|
||||
const selectedCompany = data.companies.find((c) => c.id === selected);
|
||||
const observed = data.observations.filter((o) => ["signal", "estimate"].includes(o.selection));
|
||||
const revenueCompanies = new Set(observed.map((o) => o.company)).size;
|
||||
const disclosedCompanies = new Set(observed.map((o) => o.company)).size;
|
||||
const revenueCompanies = new Set(data.observations.filter((o) => ["signal", "estimate", "platform"].includes(o.selection)).map((o) => o.company)).size;
|
||||
const selectedObservations = data.observations.filter((o) => o.company === selected);
|
||||
const selectedEvidenceIds = new Set([
|
||||
...(selectedCompany?.evidenceIds ?? []), ...(selectedCompany?.pricingEvidence ? [selectedCompany.pricingEvidence] : []),
|
||||
@@ -84,7 +86,7 @@ export default function AICompanyResearch() {
|
||||
return <div className="ai-research not-prose">
|
||||
<div className="research-meta"><span>资料截至 {data.asOf}</span><span>首批研究 · 持续补全</span></div>
|
||||
<div className="research-stats">
|
||||
<div><strong>{data.companies.length}</strong><span>已收录公司</span></div>
|
||||
<div><strong>{data.companies.length}</strong><span>公司/产品记录</span></div>
|
||||
<div><strong>{data.categories.length}</strong><span>业务类别</span></div>
|
||||
<div><strong>{revenueCompanies}</strong><span>找到收入信号的公司</span></div>
|
||||
<div><strong>{data.evidence.length}</strong><span>证据记录</span></div>
|
||||
@@ -97,7 +99,7 @@ export default function AICompanyResearch() {
|
||||
</div>
|
||||
<section aria-labelledby="research-categories-title" className="research-section">
|
||||
<h2 id="research-categories-title">业务分类</h2><p className="research-muted">点击类别查看对应公司。数量反映本次采集覆盖,不是市场份额。</p>
|
||||
<div className="research-categories">{summaries.map((c) => <button key={c.id} onClick={() => chooseCategory(c.id)} aria-label={`查看${c.name}的${c.count}家公司`}>
|
||||
<div className="research-categories">{summaries.map((c) => <button key={c.id} onClick={() => chooseCategory(c.id)} aria-label={`查看${c.name}的${c.count}条记录`}>
|
||||
<div><span>{c.name}</span><strong>{c.count}</strong></div>
|
||||
<span className="research-bar" aria-hidden="true"><i style={{ width: `${c.count / Math.max(1, ...summaries.map((s) => s.count)) * 100}%` }} /></span>
|
||||
<small>{c.description}</small>
|
||||
@@ -106,7 +108,7 @@ export default function AICompanyResearch() {
|
||||
<section ref={companySection} className="research-section" aria-labelledby="research-records-title">
|
||||
<h2 id="research-records-title">研究数据</h2>
|
||||
<div className="research-tabs" role="group" aria-label="研究数据视图">
|
||||
{([['companies','公司数据'],['revenue','收入观测'],['sources','原始证据'],['reviews','核查记录']] as const).map(([id,label]) =>
|
||||
{([['companies','公司与产品'],['revenue','收入观测'],['sources','原始证据'],['reviews','核查记录']] as const).map(([id,label]) =>
|
||||
<button key={id} aria-pressed={tab === id} onClick={() => setTab(id)}>{label}</button>)}
|
||||
</div>
|
||||
{tab === "companies" && <>
|
||||
@@ -116,10 +118,10 @@ export default function AICompanyResearch() {
|
||||
<label>成立或发布<select value={cohort} onChange={(e) => { setCohort(e.target.value); setPage(0); }}><option value="">全部组别</option><option value="2025">2025 年成立</option><option value="2026">2026 年成立</option><option value="other">公开发布与转型对照</option></select></label>
|
||||
<label>资料深度<select value={depth} onChange={(e) => { setDepth(e.target.value); setPage(0); }}><option value="">全部资料</option><option value="case">原有个案研究</option><option value="profile">目录基础资料</option></select></label>
|
||||
<label>记录团队规模<select value={teamBand} onChange={(e) => { setTeamBand(e.target.value); setPage(0); }}><option value="">全部规模</option><option value="small">1—5 人</option><option value="medium">6—20 人</option><option value="large">21—50 人</option><option value="xl">至少 51 人</option><option value="unknown">未披露或分组不确定</option></select></label>
|
||||
<label>收入资料<select value={income} onChange={(e) => { setIncome(e.target.value); setPage(0); }}><option value="">全部状态</option><option value="self">找到公司披露</option><option value="estimate">仅第三方估算</option><option value="unknown">暂无可用数字</option></select></label>
|
||||
<label>收入资料<select value={income} onChange={(e) => { setIncome(e.target.value); setPage(0); }}><option value="">全部状态</option><option value="self">找到公司披露</option><option value="estimate">仅第三方估算</option><option value="platform">仅支付平台记录</option><option value="unknown">暂无可用数字</option></select></label>
|
||||
</div>
|
||||
<div className="research-result-count" role="status">{filtered.length} 家匹配公司<button onClick={() => { setCategory(""); setQuery(""); setCohort(""); setIncome(""); setDepth(""); setTeamBand(""); setPage(0); }}>清除筛选</button></div>
|
||||
<div className="research-table-wrap"><table><caption className="sr-only">公司分类与经营资料。选择公司名称查看指标和原始证据。</caption><thead><tr><th>公司 / 地区</th><th>业务与买家</th><th>成立 / 发布组别</th><th>团队规模</th><th>商业成熟度</th><th>收入资料</th></tr></thead><tbody>
|
||||
<div className="research-result-count" role="status">{filtered.length} 条匹配记录<button onClick={() => { setCategory(""); setQuery(""); setCohort(""); setIncome(""); setDepth(""); setTeamBand(""); setPage(0); }}>清除筛选</button></div>
|
||||
<div className="research-table-wrap"><table><caption className="sr-only">公司/产品分类与经营资料。选择名称查看指标和原始证据。</caption><thead><tr><th>公司或产品 / 地区</th><th>业务与买家</th><th>成立 / 发布组别</th><th>团队规模</th><th>商业成熟度</th><th>收入资料</th></tr></thead><tbody>
|
||||
{filtered.slice(page * pageSize, (page + 1) * pageSize).map((c) => <tr key={c.id} data-selected={selected === c.id || undefined}>
|
||||
<td><button className="research-company-name" aria-expanded={selected === c.id} aria-controls="research-company-detail" onClick={() => setSelected(c.id)}>{c.name}</button><small>{c.location ?? "地区未注明"}</small></td>
|
||||
<td><span className="research-tag">{categories.get(c.cat)?.name}</span>{c.profileOnly && <small>规则初分 · 待复核</small>}<p>{c.business}</p><small>买家:{c.buyer}</small></td>
|
||||
@@ -130,22 +132,24 @@ export default function AICompanyResearch() {
|
||||
{filtered.length > pageSize && <nav className="research-pagination" aria-label="公司数据分页"><button disabled={page === 0} onClick={() => setPage((n) => n - 1)}>上一页</button><span>{page + 1} / {Math.ceil(filtered.length / pageSize)}</span><button disabled={(page + 1) * pageSize >= filtered.length} onClick={() => setPage((n) => n + 1)}>下一页</button></nav>}
|
||||
</>}
|
||||
{tab === "revenue" && <>
|
||||
<p className="research-notice">目前找到 {revenueCompanies} 家公司的收入信号。大多是缺少日历时点的历史里程碑,且 ARR 与年化收入尚未统一。领域均值和离散度暂不计算。</p>
|
||||
<IncomeDistribution data={data} onSelect={setSelected} />
|
||||
<h3>公司披露与估算的历史里程碑</h3>
|
||||
<p className="research-notice">这一组包含 {disclosedCompanies} 家公司的披露或估算信号,多为异时点历史里程碑,ARR 与年化收入口径仍待核对,暂不计算其同期领域均值。上面的平台MRR统计与这一组分开。</p>
|
||||
<div className="research-revenue-chart" aria-label="不同时间的公开收入里程碑,线性长度,非同行排名">{observed.map((o) => <div key={o.id}>
|
||||
<div><button onClick={() => setSelected(o.company)}>{o.name}</button><strong>{o.bound !== "=" && o.bound}{money(o.value, o.unit)}</strong></div>
|
||||
<span className="research-bar" data-estimate={o.selection === "estimate" || undefined} aria-hidden="true"><i style={{width: `${o.value / Math.max(...observed.map((v) => v.value)) * 100}%`}} /></span>
|
||||
<small>{o.kind} · {useLabels[o.selection]} · {o.timing}</small>
|
||||
</div>)}</div>
|
||||
<div className="research-table-wrap"><table><caption>当前研究数据中的收入可得性,目录项未逐一检索;未建立同期、同龄、同口径比较组</caption><thead><tr><th>业务分类</th><th>公司披露数</th><th>仅估算数</th><th>暂无可用数字</th><th>同期均值 / 离散度</th></tr></thead><tbody>{summaries.map((c) => <tr key={c.id}><td><button onClick={() => chooseCategory(c.id)}>{c.name}</button></td><td>{c.self}</td><td>{c.estimate}</td><td>{c.unknown}</td><td>条件不足</td></tr>)}</tbody></table></div>
|
||||
<div className="research-table-wrap"><table><caption>当前研究数据中的收入可得性,目录项未逐一检索;未建立同期、同龄、同口径比较组</caption><thead><tr><th>业务分类</th><th>公司披露数</th><th>仅估算数</th><th>仅平台数</th><th>暂无可用数字</th><th>同期均值 / 离散度</th></tr></thead><tbody>{summaries.map((c) => <tr key={c.id}><td><button onClick={() => chooseCategory(c.id)}>{c.name}</button></td><td>{c.self}</td><td>{c.estimate}</td><td>{c.platform}</td><td>{c.unknown}</td><td>条件不足</td></tr>)}</tbody></table></div>
|
||||
<p className="research-muted">离散度用于筛选值得核查的异常,还需排除公司年龄、口径和披露偏差;不能据此认定财务造假。</p>
|
||||
</>}
|
||||
{tab === "sources" && <><p className="research-muted">来源链接保留原始位置。相同 URL 的记录可能属于同一披露,不能按记录数计算独立证据数量。</p><ul className="research-evidence-list">{data.evidence.slice(sourcePage * evidencePageSize, (sourcePage + 1) * evidencePageSize).map((e) => <Evidence key={e.id} item={e} />)}</ul>{data.evidence.length > evidencePageSize && <nav className="research-pagination" aria-label="原始证据分页"><button disabled={sourcePage === 0} onClick={() => setSourcePage((n) => n - 1)}>上一页</button><span>{sourcePage + 1} / {Math.ceil(data.evidence.length / evidencePageSize)}</span><button disabled={(sourcePage + 1) * evidencePageSize >= data.evidence.length} onClick={() => setSourcePage((n) => n + 1)}>下一页</button></nav>}</>}
|
||||
{tab === "reviews" && <ul className="research-review-list">{data.reviews.map((r) => <li key={r.id}><span className="research-tag">{r.type}</span><h3>{r.company}</h3><p>{r.summary}</p><p>{r.handling}</p><div>{r.evidenceIds.map((id) => { const e=data.evidence.find((item) => item.id === id); return e ? <External key={id} url={e.url}>{id}</External> : null; })}</div></li>)}</ul>}
|
||||
</section>
|
||||
{selectedCompany && <section id="research-company-detail" ref={detailSection} tabIndex={-1} className="research-detail research-section" aria-label={`${selectedCompany.name}公司明细`}>
|
||||
{selectedCompany && <section id="research-company-detail" ref={detailSection} tabIndex={-1} className="research-detail research-section" aria-label={`${selectedCompany.name}资料明细`}>
|
||||
<div className="research-detail-heading"><h2>{selectedCompany.name}</h2><button onClick={() => setSelected(null)} aria-label="关闭公司明细">关闭</button></div>
|
||||
<p><External url={selectedCompany.url}>公司资料入口</External> {selectedCompany.extraUrl && <External url={selectedCompany.extraUrl}>额外核验入口</External>}</p>
|
||||
<dl className="research-company-facts"><div><dt>主营业务</dt><dd>{selectedCompany.business}</dd></div><div><dt>样本组别</dt><dd>{selectedCompany.cohort}</dd></div><div><dt>买家</dt><dd>{selectedCompany.buyer}</dd></div><div><dt>收费方式</dt><dd>{selectedCompany.price}</dd></div><div><dt>交付证据</dt><dd>{selectedCompany.delivery}</dd></div><div><dt>团队口径</dt><dd>{selectedCompany.teamBasis ?? "本轮未核验人数"}</dd></div><div><dt>分类依据</dt><dd>{selectedCompany.classificationBasis ?? "原有个案分类;随研究复核"}</dd></div><div><dt>技术水平</dt><dd>尚未独立测试,不据融资或收入作能力排名。</dd></div><div><dt>身份与限制</dt><dd>{selectedCompany.notes}</dd></div></dl>
|
||||
<p><External url={selectedCompany.url}>资料入口</External> {selectedCompany.extraUrl && <External url={selectedCompany.extraUrl}>额外核验入口</External>}</p>
|
||||
<dl className="research-company-facts"><div><dt>主营业务</dt><dd>{selectedCompany.business}</dd></div><div><dt>样本组别</dt><dd>{selectedCompany.cohort}</dd></div><div><dt>买家</dt><dd>{selectedCompany.buyer}</dd></div><div><dt>收费/交付模式</dt><dd>{selectedCompany.businessModel ?? "待进一步核验"}</dd></div><div><dt>收费方式</dt><dd>{selectedCompany.price}</dd></div><div><dt>交付证据</dt><dd>{selectedCompany.delivery}</dd></div><div><dt>团队口径</dt><dd>{selectedCompany.teamBasis ?? "本轮未核验人数"}</dd></div><div><dt>分类依据</dt><dd>{selectedCompany.classificationBasis ?? "原有个案分类;随研究复核"}</dd></div><div><dt>技术水平</dt><dd>尚未独立测试,不据融资或收入作能力排名。</dd></div><div><dt>观测主体</dt><dd>{selectedCompany.entityBasis ?? "公司或经营品牌,法律主体未逐一核验"}</dd></div><div><dt>身份与限制</dt><dd>{selectedCompany.notes}</dd></div></dl>
|
||||
<h3>经营指标与排除记录</h3>
|
||||
{selectedObservations.length ? <div className="research-table-wrap"><table><thead><tr><th>指标 / 归属</th><th>原始数值</th><th>数据时点</th><th>用途及限制</th><th>证据</th></tr></thead><tbody>{selectedObservations.map((o) => <tr key={o.id}><td>{o.kind}<small>{o.scope}</small></td><td>{o.bound !== "=" && o.bound}{money(o.value,o.unit)}</td><td>{o.timing}</td><td>{useLabels[o.selection] ?? o.selection}<small>{o.reason}</small></td><td><a href={`#detail-evidence-${o.evidence}`}>{o.evidence}</a></td></tr>)}</tbody></table></div> : <p>本轮尚未找到可记录的量化经营指标。收入保持未知。</p>}
|
||||
<h3>原始证据</h3><ul className="research-evidence-list">{selectedEvidence.map((e) => <Evidence key={e.id} prefix="detail-evidence" item={e} />)}</ul>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
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 <section className="research-distribution" aria-labelledby="income-distribution-title">
|
||||
<h3 id="income-distribution-title">收入分布与均值敏感性</h3>
|
||||
<p>单独比较支付平台显示的 USD MRR。每个点是一个产品或业务,法律主体与支付账户的独立性尚未全部核实;这不是行业总体或公司财报样本。</p>
|
||||
<div className="research-filters">
|
||||
<label>最后同步日期窗口<select value={dateWindow} onChange={(event) => setDateWindow(event.target.value)}><option value="7">截止日前 7 天内</option><option value="30">截止日前 30 天内</option><option value="90">截止日前 90 天内</option><option value="all">全部已知日期(混合历史)</option></select></label>
|
||||
<label>资料中的创立年<select value={year} onChange={(event) => setYear(event.target.value)}><option value="">2025 与 2026</option><option value="2025">2025</option><option value="2026">2026</option></select></label>
|
||||
<label>显示单位<select value={annualize ? "annualized" : "mrr"} onChange={(event) => setAnnualize(event.target.value === "annualized")}><option value="mrr">MRR(美元/月)</option><option value="annualized">MRR×12参考年化(美元/年)</option></select></label>
|
||||
</div>
|
||||
<div className="income-options"><label><input type="checkbox" checked={splitModels} onChange={(event) => setSplitModels(event.target.checked)} /> 按收费与交付模式细分</label><label><input type="checkbox" checked={logScale} onChange={(event) => setLogScale(event.target.checked)} /> 对数横轴</label></div>
|
||||
<p className="research-notice">资料截至 {data.asOf},纳入 {sample.points.length} 个产品,另有 {sample.excluded.length} 个 MRR 观测未纳入。{annualize ? "乘12只改变显示单位,不证明全年收入、续约或ARR;CV与相对敏感性不变。" : "MRR不等于近30日收款;平台数据连接也不等于财务审计。"}{dateWindow === "all" && " 当前包含不同时间的历史观测,不能称为同期对照。"}</p>
|
||||
{sample.points.length ? <>
|
||||
<figure>
|
||||
<div className="research-table-wrap"><svg className="income-distribution-chart" viewBox={`0 0 900 ${groups.length * 70 + 65}`} role="group" aria-label={`${metric}分布,${logScale ? "对数" : "线性"}横轴;圆点为产品,竖线为均值,菱形为中位数`}>
|
||||
{ticks.map((tick) => <g key={tick}><line x1={x(tick)} x2={x(tick)} y1={15} y2={groups.length * 70 + 10} stroke="#dfe6e0" /><text x={x(tick)} y={groups.length * 70 + 35} textAnchor="middle" fontSize="11" fill="#637066">{usd(tick)}</text></g>)}
|
||||
{groups.map((group, index) => {
|
||||
const y = index * 70 + 40;
|
||||
return <g key={group.key}>
|
||||
<text x={8} y={y - 8} fontSize="12" fill="#202724">{categories.get(group.category)}</text>
|
||||
<text x={8} y={y + 10} fontSize="10" fill="#637066">{group.provider} · {group.model} · n={group.stats.n}</text>
|
||||
<line x1={x(group.stats.min)} x2={x(group.stats.max)} y1={y} y2={y} stroke="#95b4a6" strokeWidth={2} />
|
||||
<line x1={x(group.stats.mean)} x2={x(group.stats.mean)} y1={y - 17} y2={y + 17} stroke="#b17725" strokeWidth={3}><title>样本均值 {usd(group.stats.mean)}</title></line>
|
||||
<path d={`M${x(group.stats.median)},${y - 7} l7,7 l-7,7 l-7,-7 Z`} fill="#8a78ad"><title>样本中位数 {usd(group.stats.median)}</title></path>
|
||||
{group.points.map((point, position) => <g key={point.company.id} role="button" tabIndex={0} aria-label={`查看${point.company.name},${metric} ${usd(point.value)}`} onClick={() => onSelect(point.company.id)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onSelect(point.company.id); } }}>
|
||||
<title>{point.company.name} · {usd(point.value)} · 数据日期 {point.observation.observedAt}{point.observation.connectionExpired ? " · 连接提示过期" : ""}</title>
|
||||
<circle cx={x(point.value)} cy={y + ((position % 5) - 2) * 4} r={10} fill="transparent" />
|
||||
<circle cx={x(point.value)} cy={y + ((position % 5) - 2) * 4} r={4.5} fill="#27745b" stroke="white" />
|
||||
</g>)}
|
||||
</g>;
|
||||
})}
|
||||
</svg></div>
|
||||
<figcaption>圆点:产品观测 竖线:样本均值 菱形:样本中位数。按业务类别与支付来源分组,点击点或下表产品名可看原始证据。</figcaption>
|
||||
</figure>
|
||||
<div className="research-table-wrap"><table><caption>{metric}样本统计;金额按显示值计算,CV使用样本标准差</caption><thead><tr><th>类别 / 来源 / 模式</th><th>n</th><th>均值</th><th>中位数</th><th>均值/中位数</th><th>CV</th><th>去除最大值后的均值 / 下降幅度</th><th>产品与限制</th></tr></thead><tbody>{groups.map((group) => <tr key={group.key}>
|
||||
<td>{categories.get(group.category)}<small>{group.provider} · {group.model}</small></td><td>{group.stats.n}</td><td>{usd(group.stats.mean)}</td><td>{usd(group.stats.median)}</td><td>{group.stats.meanMedianRatio?.toFixed(2) ?? "—"}</td><td>{group.stats.cv?.toFixed(2) ?? "—"}</td><td>{usd(group.stats.meanWithoutLargest)}<small>{percent(group.stats.largestRemovalDrop)}</small></td>
|
||||
<td>{group.points.map((point) => <span key={point.company.id}><button className="research-company-name" onClick={() => onSelect(point.company.id)}>{point.company.name}</button>{" "}</span>)}<small>{group.stats.n < 3 ? "样本少于3,不能据此判断领域典型收入。" : "自选的小样本,须结合产品年龄、成本与留存评估。"}</small></td>
|
||||
</tr>)}</tbody></table></div>
|
||||
</> : <p className="research-message">当前窗口和创立年份没有符合条件的 MRR 记录。</p>}
|
||||
<details><summary>统计规则与解释</summary><p>每个产品只取符合条件的最新一条平台 MRR;币种为美元,要求有效数据日期、确定数值及支付来源。公司 ARR、估算、融资、目标和近30日收入均不进入这里。未知值不补零。</p><p>CV=样本标准差÷均值,标准差分母为 n−1;n=1 不算 CV。n=2 时均值必然等于中位数,不能据此认为均值有代表性。较大的均值/中位数差距、CV或移除最大值的变化,仅提示进一步核查样本构成。</p><p>支付商与平台的退款、试用、年付、币种及指标计算可能不同(<a href="https://trustmrr.com/faq" target="_blank" rel="noopener noreferrer">平台方法说明</a>)。不能将 MRR 与收款差异、单个高值或高离散度直接解释为财务造假。比较商业模式还需要毛利、获客成本、留存、人工交付和客户集中度。</p></details>
|
||||
{sample.excluded.length > 0 && <details><summary>未纳入的 MRR 记录({sample.excluded.length})</summary><ul>{sample.excluded.map(({ observation, reason }) => <li key={observation.id}><button onClick={() => onSelect(observation.company)}>{observation.name}</button>:{usd(observation.value)} / 月,{observation.observedAt ?? "日期未知"};{reason}</li>)}</ul></details>}
|
||||
</section>;
|
||||
}
|
||||
@@ -4,14 +4,17 @@ export type ResearchCompany = {
|
||||
team: number | null; teamBound?: ">" | "≥"; location: string | null; business: string; buyer: string;
|
||||
delivery: string; notes: string; cohort: string; teamBasis: string | null;
|
||||
price: string; stage: string; sourceType: string; evidenceIds: string[];
|
||||
revenueStatus: "self" | "estimate" | "unknown";
|
||||
revenueStatus: "self" | "estimate" | "platform" | "unknown";
|
||||
extraUrl?: string; pricingEvidence?: string;
|
||||
businessModel?: string; entityBasis?: string;
|
||||
profileOnly?: boolean; classificationBasis?: string; directoryTags?: string[];
|
||||
};
|
||||
export type ResearchObservation = {
|
||||
id: string; company: string; name: string; kind: string; value: number;
|
||||
unit: string; timing: string; evidence: string; scope: string;
|
||||
selection: string; reason: string; bound: string; comparisonGroup: string | null;
|
||||
metric?: "mrr" | "arr" | "revenue-30d" | "annualized-revenue";
|
||||
observedAt?: string; provider?: string; connectionExpired?: boolean; origin?: "payment-platform";
|
||||
};
|
||||
export type ResearchEvidence = {
|
||||
id: string; companyId: string | null; title: string; url: string;
|
||||
@@ -88,6 +91,7 @@ export function categorySummary(snapshot: ResearchSnapshot) {
|
||||
const companies = snapshot.companies.filter((c) => c.cat === category.id);
|
||||
return { ...category, count: companies.length,
|
||||
self: companies.filter((c) => c.revenueStatus === "self").length,
|
||||
platform: companies.filter((c) => c.revenueStatus === "platform").length,
|
||||
estimate: companies.filter((c) => c.revenueStatus === "estimate").length,
|
||||
unknown: companies.filter((c) => c.revenueStatus === "unknown").length };
|
||||
});
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { ResearchCompany, ResearchObservation, ResearchSnapshot } from "./ai-research";
|
||||
|
||||
export type IncomePoint = { company: ResearchCompany; observation: ResearchObservation; value: number };
|
||||
export type IncomeStats = {
|
||||
n: number; mean: number; median: number; min: number; max: number;
|
||||
sampleDeviation: number | null; cv: number | null; meanMedianRatio: number | null;
|
||||
largestShare: number | null; meanWithoutLargest: number | null; largestRemovalDrop: number | null;
|
||||
};
|
||||
|
||||
/** Describes the supplied sample only; missing values never become zero. */
|
||||
export function describeIncome(values: number[]): IncomeStats | null {
|
||||
if (!values.length || values.some((value) => !Number.isFinite(value) || value < 0)) return null;
|
||||
const ordered = [...values].sort((a, b) => a - b);
|
||||
const n = ordered.length;
|
||||
const total = ordered.reduce((sum, value) => sum + value, 0);
|
||||
const mean = total / n;
|
||||
const median = n % 2 ? ordered[(n - 1) / 2] : (ordered[n / 2 - 1] + ordered[n / 2]) / 2;
|
||||
const sampleDeviation = n > 1 ? Math.sqrt(ordered.reduce((sum, value) => sum + (value - mean) ** 2, 0) / (n - 1)) : null;
|
||||
const max = ordered[n - 1];
|
||||
const meanWithoutLargest = n > 1 ? (total - max) / (n - 1) : null;
|
||||
return { n, mean, median, min: ordered[0], max, sampleDeviation,
|
||||
cv: sampleDeviation !== null && mean > 0 ? sampleDeviation / mean : null,
|
||||
meanMedianRatio: median > 0 ? mean / median : null,
|
||||
largestShare: total > 0 ? max / total : null, meanWithoutLargest,
|
||||
largestRemovalDrop: meanWithoutLargest !== null && mean > 0 ? (mean - meanWithoutLargest) / mean : null };
|
||||
}
|
||||
|
||||
function calendarDay(value: string | undefined): number | null {
|
||||
if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
|
||||
const time = Date.parse(`${value}T00:00:00Z`);
|
||||
return Number.isFinite(time) && new Date(time).toISOString().slice(0, 10) === value ? time / 86400000 : null;
|
||||
}
|
||||
|
||||
/** Latest dated platform MRR per product, never ARR, cash collections or estimates. */
|
||||
export function platformIncomeSample(snapshot: ResearchSnapshot, options: {
|
||||
windowDays: number | null; foundedYear?: number; annualize?: boolean;
|
||||
}) {
|
||||
const end = calendarDay(snapshot.asOf);
|
||||
if (end === null || (options.windowDays !== null && (!Number.isInteger(options.windowDays) || options.windowDays < 0)))
|
||||
throw new Error("Invalid income sample window");
|
||||
const companies = new Map(snapshot.companies.map((company) => [company.id, company]));
|
||||
const latest = new Map<string, ResearchObservation>();
|
||||
const excluded: { observation: ResearchObservation; reason: string }[] = [];
|
||||
const candidates = snapshot.observations.filter((o) => o.selection === "platform" && o.metric === "mrr");
|
||||
for (const observation of candidates) {
|
||||
const company = companies.get(observation.company);
|
||||
const day = calendarDay(observation.observedAt);
|
||||
let reason = "";
|
||||
if (!company || ![2025, 2026].includes(company.year ?? 0)) reason = "创立年份未纳入2025—2026组";
|
||||
else if (options.foundedYear && company.year !== options.foundedYear) reason = "不属于所选创立年份";
|
||||
else if (observation.origin !== "payment-platform" || observation.unit !== "USD" || observation.bound !== "=" ||
|
||||
observation.comparisonGroup !== "trustmrr-usd-mrr" || !observation.provider || !Number.isFinite(observation.value) || observation.value < 0)
|
||||
reason = "来源、币种、指标或数值口径不满足条件";
|
||||
else if (day === null) reason = "缺少有效数据日期";
|
||||
else if (day > end) reason = "数据日期晚于研究截止日";
|
||||
else if (options.windowDays !== null && end - day > options.windowDays) reason = "最后同步日期超出所选窗口";
|
||||
if (reason) { excluded.push({ observation, reason }); continue; }
|
||||
const previous = latest.get(observation.company);
|
||||
if (!previous || observation.observedAt! > previous.observedAt!) {
|
||||
if (previous) excluded.push({ observation: previous, reason: "同一产品的较早观测" });
|
||||
latest.set(observation.company, observation);
|
||||
} else excluded.push({ observation, reason: "同一产品已选较新或同日观测" });
|
||||
}
|
||||
const points: IncomePoint[] = [...latest.values()].map((observation) => ({
|
||||
company: companies.get(observation.company)!, observation,
|
||||
value: observation.value * (options.annualize ? 12 : 1),
|
||||
}));
|
||||
return { points, excluded };
|
||||
}
|
||||
|
||||
export function groupIncome(points: IncomePoint[], splitModels = true) {
|
||||
const groups = new Map<string, { category: string; provider: string; model: string; points: IncomePoint[] }>();
|
||||
for (const point of points) {
|
||||
const category = point.company.cat;
|
||||
const provider = point.observation.provider!;
|
||||
const model = splitModels ? point.company.businessModel ?? "收费模式未核验" : "全部收费模式";
|
||||
const key = JSON.stringify([category, provider, model]);
|
||||
if (!groups.has(key)) groups.set(key, { category, provider, model, points: [] });
|
||||
groups.get(key)!.points.push(point);
|
||||
}
|
||||
return [...groups.entries()].map(([key, group]) => ({ key, ...group, stats: describeIncome(group.points.map((point) => point.value))! }));
|
||||
}
|
||||
@@ -1470,3 +1470,13 @@ svg { flex-shrink: 0; }
|
||||
.research-tabs { gap: 16px; }
|
||||
.research-layout .reading-main { padding: 22px 16px 0; }
|
||||
}
|
||||
|
||||
.research-distribution { margin: 24px 0 38px; }
|
||||
.research-distribution figure { margin: 20px 0; }
|
||||
.income-distribution-chart { display: block; width: 100%; min-width: 900px; height: auto; }
|
||||
.income-distribution-chart [role="button"] { cursor: pointer; outline: none; }
|
||||
.income-distribution-chart [role="button"]:focus circle:last-child { stroke: #b17725; stroke-width: 3; }
|
||||
.research-distribution figcaption { color: var(--muted); font-size: 11px; line-height: 1.7; margin: 10px 0; }
|
||||
.income-options { display: flex; flex-wrap: wrap; gap: 18px; margin: 12px 0; font-size: 12px; }
|
||||
.income-options label { display: flex; gap: 6px; align-items: center; }
|
||||
.research-distribution details { margin: 16px 0; font-size: 12px; line-height: 1.8; }
|
||||
|
||||
@@ -5,11 +5,13 @@ import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import seed from "../../../content/research/ai-companies.seed.json";
|
||||
import coverageAudit from "../../../content/research/ai-companies.coverage.json";
|
||||
import incomeReference from "../../../content/research/ai-income-analysis.json";
|
||||
import { readResearchSnapshot } from "../src/lib/ai-research-store";
|
||||
import { openSiteDatabase } from "../src/lib/site-database";
|
||||
import { categorySummary, evidenceUrl, validateResearch, type ResearchSnapshot } from "../src/lib/ai-research";
|
||||
import { consumeCode, initializeStore } from "../src/lib/auth-store";
|
||||
import { newAuthConfig } from "../src/lib/auth";
|
||||
import { describeIncome, groupIncome, platformIncomeSample } from "../src/lib/research-statistics";
|
||||
|
||||
test("research revisions share the authentication database and preserve replay limits", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "site-research-"));
|
||||
@@ -21,9 +23,9 @@ test("research revisions share the authentication database and preserve replay l
|
||||
for (let i = 0; i < 20; i++) consumeCode(config, "invalid");
|
||||
assert.equal(consumeCode(config, "invalid"), "limited");
|
||||
const initial = readResearchSnapshot();
|
||||
assert.equal(initial.companies.length, 986);
|
||||
assert.equal(initial.evidence.length, 1035);
|
||||
assert.equal(initial.observations.length, 75);
|
||||
assert.equal(initial.companies.length, 1002);
|
||||
assert.equal(initial.evidence.length, 1055);
|
||||
assert.equal(initial.observations.length, 126);
|
||||
assert.deepEqual(readResearchSnapshot(), initial, "reopening does not duplicate records");
|
||||
const db = openSiteDatabase();
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[];
|
||||
@@ -61,9 +63,10 @@ test("research validation rejects broken evidence and unsafe source links", () =
|
||||
test("disclosure counts exclude client income, biographies, targets and duplicate milestones", () => {
|
||||
const snapshot = seed as ResearchSnapshot;
|
||||
const summary = categorySummary(snapshot);
|
||||
assert.equal(summary.reduce((n, c) => n + c.count, 0), 986);
|
||||
assert.equal(summary.reduce((n, c) => n + c.count, 0), 1002);
|
||||
assert.equal(summary.reduce((n, c) => n + c.self, 0), 8);
|
||||
assert.equal(summary.reduce((n, c) => n + c.estimate, 0), 1);
|
||||
assert.equal(summary.reduce((n, c) => n + c.platform, 0), 16);
|
||||
assert.equal(summary.reduce((n, c) => n + c.unknown, 0), 977);
|
||||
assert.equal(snapshot.companies.find((c) => c.name === "Rork")?.revenueStatus, "unknown");
|
||||
assert.equal(snapshot.companies.find((c) => c.name === "Result")?.revenueStatus, "unknown");
|
||||
@@ -110,3 +113,74 @@ test("directory coverage reconciles every candidate and keeps profile-only incom
|
||||
invalid.coverage!.newProfiles += 1;
|
||||
assert.throws(() => validateResearch(invalid), /coverage accounting/);
|
||||
});
|
||||
|
||||
test("income descriptions report sample dispersion and maximum-value sensitivity", () => {
|
||||
const stats = describeIncome([100, 200, 300])!;
|
||||
assert.equal(stats.mean, 200);
|
||||
assert.equal(stats.median, 200);
|
||||
assert.equal(stats.sampleDeviation, 100);
|
||||
assert.equal(stats.cv, .5);
|
||||
assert.equal(stats.largestShare, .5);
|
||||
assert.equal(stats.meanWithoutLargest, 150);
|
||||
assert.equal(stats.largestRemovalDrop, .25);
|
||||
assert.equal(describeIncome([5])!.cv, null);
|
||||
assert.equal(describeIncome([0, 0])!.mean, 0, "observed zero is distinct from missing");
|
||||
assert.equal(describeIncome([0, 0])!.cv, null);
|
||||
assert.equal(describeIncome([]), null);
|
||||
assert.equal(describeIncome([100, NaN]), null);
|
||||
assert.equal(describeIncome([-1]), null);
|
||||
assert.ok(describeIncome([1, 2, 100])!.largestRemovalDrop! > .95);
|
||||
});
|
||||
|
||||
test("platform samples exclude stale data and keep ARR, estimates and collections separate", () => {
|
||||
const snapshot = seed as ResearchSnapshot;
|
||||
const recent = platformIncomeSample(snapshot, { windowDays: 7 });
|
||||
assert.equal(recent.points.length, 14);
|
||||
assert.equal(recent.excluded.length, 3);
|
||||
assert.ok(recent.points.every((p) => p.observation.metric === "mrr" && p.observation.selection === "platform"));
|
||||
assert.deepEqual(new Set(recent.excluded.map((p) => p.observation.company)), new Set(["AI016", "MR013", "MR014"]));
|
||||
assert.ok(recent.points.some((p) => p.company.name === "TrackAI" && p.observation.connectionExpired), "dated recent value is not a claim of active connection");
|
||||
const older = platformIncomeSample(snapshot, { windowDays: 90 });
|
||||
assert.equal(older.points.length, 15);
|
||||
assert.ok(older.points.some((p) => p.company.id === "AI016"));
|
||||
const scaled = platformIncomeSample(snapshot, { windowDays: 7, annualize: true });
|
||||
const monthlyGroups = groupIncome(recent.points);
|
||||
const annualGroups = groupIncome(scaled.points);
|
||||
for (const group of monthlyGroups) {
|
||||
const annual = annualGroups.find((g) => g.key === group.key)!;
|
||||
assert.ok(Math.abs(annual.stats.mean - group.stats.mean * 12) < 1e-6);
|
||||
if (group.stats.cv !== null) assert.ok(Math.abs(annual.stats.cv! - group.stats.cv) < 1e-10);
|
||||
}
|
||||
const invalid = structuredClone(snapshot);
|
||||
invalid.observations = invalid.observations.filter((o) => o.company === "MR001" && o.metric === "mrr");
|
||||
const first = invalid.observations[0];
|
||||
invalid.observations.push({ ...first, id: "future", observedAt: "2026-11-01", value: 999999 });
|
||||
invalid.observations.push({ ...first, id: "bad-date", observedAt: "2026-02-30", value: 999999 });
|
||||
invalid.observations.push({ ...first, id: "currency", unit: "EUR", value: 999999 });
|
||||
invalid.observations.push({ ...first, id: "older", observedAt: "2026-10-28", value: 1 });
|
||||
const selected = platformIncomeSample(invalid, { windowDays: 7 });
|
||||
assert.equal(selected.points.length, 1);
|
||||
assert.equal(selected.points[0].observation.id, first.id, "bad newer data and older duplicate must not replace latest valid point");
|
||||
assert.equal(selected.excluded.length, 4);
|
||||
assert.throws(() => platformIncomeSample(snapshot, { windowDays: -1 }), /window/);
|
||||
});
|
||||
|
||||
test("distribution statistics match independently calculated snapshot references", () => {
|
||||
const snapshot = seed as ResearchSnapshot;
|
||||
assert.equal(incomeReference.sourceVersion, snapshot.version);
|
||||
for (const reference of incomeReference.windows) {
|
||||
const sample = platformIncomeSample(snapshot, { windowDays: reference.days });
|
||||
assert.deepEqual(new Set(sample.points.map((p) => p.company.id)), new Set(reference.products));
|
||||
const groups = groupIncome(sample.points);
|
||||
assert.equal(groups.length, reference.groups.length);
|
||||
for (const expected of reference.groups) {
|
||||
const actual = groups.find((g) => g.category === expected.category && g.provider === expected.provider && g.model === expected.model)!;
|
||||
assert.equal(actual.stats.n, expected.n);
|
||||
for (const metric of ["mean", "median", "sampleDeviation", "cv", "meanWithoutLargest", "largestRemovalDrop"] as const) {
|
||||
const value = expected[metric];
|
||||
if (value === null) assert.equal(actual.stats[metric], null);
|
||||
else assert.ok(Math.abs(actual.stats[metric]! - value) < 1e-6, metric);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"sourceVersion": "2026-10-30-income-1",
|
||||
"asOf": "2026-10-30",
|
||||
"metric": "platform-mrr-usd",
|
||||
"sampleUnit": "product-or-business",
|
||||
"note": "独立Python复算参考值,仅验证样本计算;不是行业估计或财务审计。",
|
||||
"windows": [
|
||||
{
|
||||
"days": 7,
|
||||
"products": [
|
||||
"MR001",
|
||||
"MR002",
|
||||
"MR003",
|
||||
"MR004",
|
||||
"MR005",
|
||||
"MR006",
|
||||
"MR007",
|
||||
"MR008",
|
||||
"MR009",
|
||||
"MR010",
|
||||
"MR011",
|
||||
"MR012",
|
||||
"MR015",
|
||||
"MR016"
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"category": "C06",
|
||||
"provider": "Stripe",
|
||||
"model": "软件订阅",
|
||||
"products": [
|
||||
"MR011",
|
||||
"MR012",
|
||||
"MR015",
|
||||
"MR016"
|
||||
],
|
||||
"n": 4,
|
||||
"mean": 34815.0,
|
||||
"median": 36723.0,
|
||||
"sampleDeviation": 16334.54845820151,
|
||||
"cv": 0.46918134304758036,
|
||||
"meanWithoutLargest": 28940.666666666668,
|
||||
"largestRemovalDrop": 0.16872995356407675
|
||||
},
|
||||
{
|
||||
"category": "C08",
|
||||
"provider": "RevenueCat",
|
||||
"model": "消费者订阅",
|
||||
"products": [
|
||||
"MR001"
|
||||
],
|
||||
"n": 1,
|
||||
"mean": 53101.0,
|
||||
"median": 53101,
|
||||
"sampleDeviation": null,
|
||||
"cv": null,
|
||||
"meanWithoutLargest": null,
|
||||
"largestRemovalDrop": null
|
||||
},
|
||||
{
|
||||
"category": "C08",
|
||||
"provider": "Stripe",
|
||||
"model": "内容制作服务",
|
||||
"products": [
|
||||
"MR002"
|
||||
],
|
||||
"n": 1,
|
||||
"mean": 62423.0,
|
||||
"median": 62423,
|
||||
"sampleDeviation": null,
|
||||
"cv": null,
|
||||
"meanWithoutLargest": null,
|
||||
"largestRemovalDrop": null
|
||||
},
|
||||
{
|
||||
"category": "C08",
|
||||
"provider": "Stripe",
|
||||
"model": "软件订阅",
|
||||
"products": [
|
||||
"MR003",
|
||||
"MR004",
|
||||
"MR005"
|
||||
],
|
||||
"n": 3,
|
||||
"mean": 41693.0,
|
||||
"median": 49069,
|
||||
"sampleDeviation": 28111.375064197768,
|
||||
"cv": 0.6742468775141575,
|
||||
"meanWithoutLargest": 29849.0,
|
||||
"largestRemovalDrop": 0.2840764636749574
|
||||
},
|
||||
{
|
||||
"category": "C09",
|
||||
"provider": "RevenueCat",
|
||||
"model": "消费者订阅",
|
||||
"products": [
|
||||
"MR006",
|
||||
"MR007",
|
||||
"MR008",
|
||||
"MR009",
|
||||
"MR010"
|
||||
],
|
||||
"n": 5,
|
||||
"mean": 25349.2,
|
||||
"median": 20483,
|
||||
"sampleDeviation": 11942.947278624319,
|
||||
"cv": 0.4711370488466823,
|
||||
"meanWithoutLargest": 20046.0,
|
||||
"largestRemovalDrop": 0.20920581320120557
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"days": 90,
|
||||
"products": [
|
||||
"MR001",
|
||||
"MR002",
|
||||
"MR003",
|
||||
"MR004",
|
||||
"MR005",
|
||||
"MR006",
|
||||
"MR007",
|
||||
"MR008",
|
||||
"MR009",
|
||||
"MR010",
|
||||
"MR011",
|
||||
"MR012",
|
||||
"MR015",
|
||||
"MR016",
|
||||
"AI016"
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"category": "C06",
|
||||
"provider": "Stripe",
|
||||
"model": "软件订阅",
|
||||
"products": [
|
||||
"MR011",
|
||||
"MR012",
|
||||
"MR015",
|
||||
"MR016",
|
||||
"AI016"
|
||||
],
|
||||
"n": 5,
|
||||
"mean": 112725.6,
|
||||
"median": 40112,
|
||||
"sampleDeviation": 174786.78744916618,
|
||||
"cv": 1.5505509613536426,
|
||||
"meanWithoutLargest": 34815.0,
|
||||
"largestRemovalDrop": 0.6911526751687284
|
||||
},
|
||||
{
|
||||
"category": "C08",
|
||||
"provider": "RevenueCat",
|
||||
"model": "消费者订阅",
|
||||
"products": [
|
||||
"MR001"
|
||||
],
|
||||
"n": 1,
|
||||
"mean": 53101.0,
|
||||
"median": 53101,
|
||||
"sampleDeviation": null,
|
||||
"cv": null,
|
||||
"meanWithoutLargest": null,
|
||||
"largestRemovalDrop": null
|
||||
},
|
||||
{
|
||||
"category": "C08",
|
||||
"provider": "Stripe",
|
||||
"model": "内容制作服务",
|
||||
"products": [
|
||||
"MR002"
|
||||
],
|
||||
"n": 1,
|
||||
"mean": 62423.0,
|
||||
"median": 62423,
|
||||
"sampleDeviation": null,
|
||||
"cv": null,
|
||||
"meanWithoutLargest": null,
|
||||
"largestRemovalDrop": null
|
||||
},
|
||||
{
|
||||
"category": "C08",
|
||||
"provider": "Stripe",
|
||||
"model": "软件订阅",
|
||||
"products": [
|
||||
"MR003",
|
||||
"MR004",
|
||||
"MR005"
|
||||
],
|
||||
"n": 3,
|
||||
"mean": 41693.0,
|
||||
"median": 49069,
|
||||
"sampleDeviation": 28111.375064197768,
|
||||
"cv": 0.6742468775141575,
|
||||
"meanWithoutLargest": 29849.0,
|
||||
"largestRemovalDrop": 0.2840764636749574
|
||||
},
|
||||
{
|
||||
"category": "C09",
|
||||
"provider": "RevenueCat",
|
||||
"model": "消费者订阅",
|
||||
"products": [
|
||||
"MR006",
|
||||
"MR007",
|
||||
"MR008",
|
||||
"MR009",
|
||||
"MR010"
|
||||
],
|
||||
"n": 5,
|
||||
"mean": 25349.2,
|
||||
"median": 20483,
|
||||
"sampleDeviation": 11942.947278624319,
|
||||
"cv": 0.4711370488466823,
|
||||
"meanWithoutLargest": 20046.0,
|
||||
"largestRemovalDrop": 0.20920581320120557
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user