feat(analytics): implement logarithmic scale for response length histogram

This commit is contained in:
Kyush 2026-04-23 02:19:05 +09:00
commit bed925ef4c
6 changed files with 18 additions and 7 deletions

View file

@ -288,6 +288,7 @@ export const Analytics: Component = () => {
<MetaCluster
items={[
{ key: 'Metric', value: 'completion_tokens' },
{ key: 'Scale', value: 'Log' },
]}
/>
<HistogramChart data={histogram() ?? []} />
@ -296,6 +297,7 @@ export const Analytics: Component = () => {
<Panel title="Daily Response Length Spread" description="Completion token box plot by day using min / q1 / median / q3 / max summary.">
<MetaCluster
items={[
{ key: 'Scale', value: 'Log' },
{ key: 'Outliers', value: 'Hidden in this view' },
]}
/>

View file

@ -103,6 +103,10 @@ function formatPercent(value: number): string {
return `${value.toFixed(1)}%`;
}
function getSymlogMax(value: number): number {
return value <= 0 ? 1 : value * 1.1;
}
function getDateTicks(values: Date[], width: number): Date[] {
if (values.length <= 7) {
return values;
@ -645,11 +649,11 @@ export function HistogramChart(props: HistogramChartProps) {
const xScale = createMemo(() => {
const min = d3.min(props.data, (bin: { bin_start: number; bin_end: number; count: number }) => bin.bin_start) ?? 0;
const max = d3.max(props.data, (bin: { bin_start: number; bin_end: number; count: number }) => bin.bin_end) ?? 1;
return d3.scaleLinear().domain([min, max]).range([dimensions().marginLeft, dimensions().marginLeft + getInnerWidth(dimensions())]);
return d3.scaleSymlog().domain([Math.max(0, min), getSymlogMax(max)]).range([dimensions().marginLeft, dimensions().marginLeft + getInnerWidth(dimensions())]);
});
const yScale = createMemo(() => {
const max = d3.max(props.data, (bin: { bin_start: number; bin_end: number; count: number }) => bin.count) ?? 0;
return d3.scaleLinear().domain([0, max === 0 ? 1 : max * 1.1]).range([dimensions().marginTop + getInnerHeight(dimensions()), dimensions().marginTop]);
return d3.scaleSymlog().domain([0, getSymlogMax(max)]).range([dimensions().marginTop + getInnerHeight(dimensions()), dimensions().marginTop]);
});
const xTicks = createMemo(() => xScale().ticks(Math.max(2, Math.floor(getInnerWidth(dimensions()) / 100))));
@ -734,7 +738,7 @@ export function BoxPlotChart(props: BoxPlotChartProps) {
});
const yScale = createMemo(() => {
const max = d3.max(points(), (point: ParsedBoxPlotDatum) => point.max) ?? 0;
return d3.scaleLinear().domain([0, max === 0 ? 1 : max * 1.1]).range([dimensions().marginTop + getInnerHeight(dimensions()), dimensions().marginTop]);
return d3.scaleSymlog().domain([0, getSymlogMax(max)]).range([dimensions().marginTop + getInnerHeight(dimensions()), dimensions().marginTop]);
});
return (

View file

@ -27,4 +27,5 @@
- 모델 추이의 모델 키는 `response_model -> routed_model -> request_model -> unknown` 순서로 결정한다.
- response length 계열 시각화는 `completion_tokens` 값이 있는 요청만 집계한다.
- response length 계열 시각화는 긴 꼬리 분포를 읽기 쉽도록 로그 계열 스케일을 사용한다.
- 상세 요청 단위의 latency/body 확인은 계속 `DetailLogs` 화면에서 담당한다.

View file

@ -128,6 +128,7 @@
- `model-trends``response_model -> routed_model -> request_model -> unknown` 순서로 모델 키를 결정한다.
- response length 계열 endpoint는 `completion_tokens` 가 있는 요청만 집계한다.
- `response-length-histogram` 은 긴 꼬리 분포를 읽기 쉽도록 로그 간격 bin을 반환한다.
- 자세한 내용은 [docs/analytics.md](./analytics.md) 참고.
### Dashboard Summary

View file

@ -73,6 +73,7 @@ server/src/
- `AnalyticsService``analytics.db` 의 일별 집계와 `request_logs_YYYY-MM.db` 의 범위 조회를 함께 사용해 시계열/분포 데이터를 만든다.
- 모델 추이 키는 `response_model -> routed_model -> request_model -> unknown` 순서로 결정한다.
- response length 계열 집계는 `completion_tokens` 가 있는 요청만 포함한다.
- response length histogram은 긴 꼬리 분포를 위해 로그 간격 bin을 사용한다.
- 자세한 화면/API 설명은 [docs/analytics.md](./analytics.md) 참고.
## Deployment Notes

View file

@ -338,15 +338,17 @@ export class AnalyticsService {
return [{ bin_start: min, bin_end: max, count: values.length }];
}
const width = (max - min) / safeBinCount;
const transformedMin = Math.log1p(min);
const transformedMax = Math.log1p(max);
const width = (transformedMax - transformedMin) / safeBinCount;
const histogram = Array.from({ length: safeBinCount }, (_, index) => ({
bin_start: min + width * index,
bin_end: index === safeBinCount - 1 ? max : min + width * (index + 1),
bin_start: Math.expm1(transformedMin + width * index),
bin_end: index === safeBinCount - 1 ? max : Math.expm1(transformedMin + width * (index + 1)),
count: 0,
}));
for (const value of values) {
const index = Math.min(safeBinCount - 1, Math.floor((value - min) / width));
const index = Math.min(safeBinCount - 1, Math.floor((Math.log1p(value) - transformedMin) / width));
histogram[index].count += 1;
}