fundamental commit
APIの中にmean, variance, std, unique, count, percentile, median, mode, topn, max, min, percent-change, correlationの計算実現します。
This commit is contained in:
commit
a1ceeb29ef
1 changed files with 385 additions and 0 deletions
385
server_ok.ts
Normal file
385
server_ok.ts
Normal file
|
|
@ -0,0 +1,385 @@
|
||||||
|
// package.json dependencies needed:
|
||||||
|
// npm install express mathjs lodash
|
||||||
|
// npm install -D @types/express @types/node @types/lodash typescript ts-node
|
||||||
|
|
||||||
|
import express from 'express';
|
||||||
|
import * as math from 'mathjs';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Types for our data structures
|
||||||
|
interface DataSeries {
|
||||||
|
values: number[];
|
||||||
|
labels?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Condition {
|
||||||
|
field: string;
|
||||||
|
operator: '>' | '<' | '=' | '>=' | '<=' | '!=';
|
||||||
|
value: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for error handling
|
||||||
|
const handleError = (error: unknown): string => {
|
||||||
|
return error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Core statistical functions
|
||||||
|
class AnalyticsEngine {
|
||||||
|
|
||||||
|
// Apply conditions to filter data
|
||||||
|
private applyConditions(series: DataSeries, conditions: Condition[] = []): number[] {
|
||||||
|
if (conditions.length === 0) return series.values;
|
||||||
|
|
||||||
|
// For now, just return all values - you'd implement condition logic here
|
||||||
|
// This would involve checking conditions against associated metadata
|
||||||
|
return series.values;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicates from series
|
||||||
|
unique(series: DataSeries): number[] {
|
||||||
|
return _.uniq(series.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate mean with optional conditions
|
||||||
|
mean(series: DataSeries, conditions: Condition[] = []): number {
|
||||||
|
const filteredValues = this.applyConditions(series, conditions);
|
||||||
|
if (filteredValues.length === 0) throw new Error('No data points match conditions');
|
||||||
|
|
||||||
|
return Number(math.mean(filteredValues));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count values with optional conditions
|
||||||
|
count(series: DataSeries, conditions: Condition[] = []): number {
|
||||||
|
const filteredValues = this.applyConditions(series, conditions);
|
||||||
|
return filteredValues.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate variance
|
||||||
|
variance(series: DataSeries, conditions: Condition[] = []): number {
|
||||||
|
const filteredValues = this.applyConditions(series, conditions);
|
||||||
|
if (filteredValues.length === 0) throw new Error('No data points match conditions');
|
||||||
|
|
||||||
|
return Number(math.variance(filteredValues));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate standard deviation
|
||||||
|
standardDeviation(series: DataSeries, conditions: Condition[] = []): number {
|
||||||
|
const filteredValues = this.applyConditions(series, conditions);
|
||||||
|
if (filteredValues.length === 0) throw new Error('No data points match conditions');
|
||||||
|
|
||||||
|
return Number(math.std(filteredValues));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate percentile/quantile
|
||||||
|
percentile(
|
||||||
|
series: DataSeries,
|
||||||
|
percent: number,
|
||||||
|
ascending: boolean = true,
|
||||||
|
conditions: Condition[] = []
|
||||||
|
): number {
|
||||||
|
const filteredValues = this.applyConditions(series, conditions);
|
||||||
|
if (filteredValues.length === 0) throw new Error('No data points match conditions');
|
||||||
|
|
||||||
|
const sorted = ascending ?
|
||||||
|
_.sortBy(filteredValues) :
|
||||||
|
_.sortBy(filteredValues).reverse();
|
||||||
|
|
||||||
|
const index = (percent / 100) * (sorted.length - 1);
|
||||||
|
const lower = Math.floor(index);
|
||||||
|
const upper = Math.ceil(index);
|
||||||
|
const weight = index % 1;
|
||||||
|
|
||||||
|
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate median (50th percentile)
|
||||||
|
median(series: DataSeries, conditions: Condition[] = []): number {
|
||||||
|
return this.percentile(series, 50, true, conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate mode (most frequent value)
|
||||||
|
mode(series: DataSeries, conditions: Condition[] = []): number[] {
|
||||||
|
const filteredValues = this.applyConditions(series, conditions);
|
||||||
|
const frequency = _.countBy(filteredValues);
|
||||||
|
const maxFreq = Math.max(...Object.values(frequency));
|
||||||
|
|
||||||
|
return Object.keys(frequency)
|
||||||
|
.filter(key => frequency[key] === maxFreq)
|
||||||
|
.map(Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rank values and get top N
|
||||||
|
topN(
|
||||||
|
series: DataSeries,
|
||||||
|
n: number,
|
||||||
|
ascending: boolean = false,
|
||||||
|
conditions: Condition[] = []
|
||||||
|
): number[] {
|
||||||
|
const filteredValues = this.applyConditions(series, conditions);
|
||||||
|
const sorted = ascending ?
|
||||||
|
_.sortBy(filteredValues) :
|
||||||
|
_.sortBy(filteredValues).reverse();
|
||||||
|
|
||||||
|
return sorted.slice(0, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get maximum value
|
||||||
|
max(series: DataSeries, conditions: Condition[] = []): number {
|
||||||
|
const filteredValues = this.applyConditions(series, conditions);
|
||||||
|
if (filteredValues.length === 0) throw new Error('No data points match conditions');
|
||||||
|
|
||||||
|
return Math.max(...filteredValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get minimum value
|
||||||
|
min(series: DataSeries, conditions: Condition[] = []): number {
|
||||||
|
const filteredValues = this.applyConditions(series, conditions);
|
||||||
|
if (filteredValues.length === 0) throw new Error('No data points match conditions');
|
||||||
|
|
||||||
|
return Math.min(...filteredValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate percent change
|
||||||
|
percentChange(series: DataSeries, step: number = 1): number[] {
|
||||||
|
const values = series.values;
|
||||||
|
const changes: number[] = [];
|
||||||
|
|
||||||
|
for (let i = step; i < values.length; i++) {
|
||||||
|
const change = ((values[i] - values[i - step]) / values[i - step]) * 100;
|
||||||
|
changes.push(change);
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic correlation between two series
|
||||||
|
correlation(series1: DataSeries, series2: DataSeries): number {
|
||||||
|
if (series1.values.length !== series2.values.length) {
|
||||||
|
throw new Error('Series must have same length for correlation');
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = series1.values;
|
||||||
|
const y = series2.values;
|
||||||
|
const n = x.length;
|
||||||
|
|
||||||
|
const sumX = _.sum(x);
|
||||||
|
const sumY = _.sum(y);
|
||||||
|
const sumXY = _.sum(x.map((xi, i) => xi * y[i]));
|
||||||
|
const sumX2 = _.sum(x.map(xi => xi * xi));
|
||||||
|
const sumY2 = _.sum(y.map(yi => yi * yi));
|
||||||
|
|
||||||
|
const numerator = n * sumXY - sumX * sumY;
|
||||||
|
const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
|
||||||
|
|
||||||
|
return numerator / denominator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize analytics engine
|
||||||
|
const analytics = new AnalyticsEngine();
|
||||||
|
|
||||||
|
// API Routes
|
||||||
|
app.get('/api/health', (req, res) => {
|
||||||
|
res.json({ status: 'OK', timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unique values endpoint
|
||||||
|
app.post('/api/unique', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { series }: { series: DataSeries } = req.body;
|
||||||
|
const result = analytics.unique(series);
|
||||||
|
res.json({ success: true, data: result } as ApiResponse<number[]>);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = handleError(error);
|
||||||
|
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mean calculation endpoint
|
||||||
|
app.post('/api/mean', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body;
|
||||||
|
const result = analytics.mean(series, conditions);
|
||||||
|
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = handleError(error);
|
||||||
|
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Count endpoint
|
||||||
|
app.post('/api/count', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body;
|
||||||
|
const result = analytics.count(series, conditions);
|
||||||
|
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = handleError(error);
|
||||||
|
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Variance endpoint
|
||||||
|
app.post('/api/variance', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body;
|
||||||
|
const result = analytics.variance(series, conditions);
|
||||||
|
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = handleError(error);
|
||||||
|
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Standard deviation endpoint
|
||||||
|
app.post('/api/std', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body;
|
||||||
|
const result = analytics.standardDeviation(series, conditions);
|
||||||
|
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = handleError(error);
|
||||||
|
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Percentile endpoint
|
||||||
|
app.post('/api/percentile', (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
series,
|
||||||
|
percent,
|
||||||
|
ascending = true,
|
||||||
|
conditions = []
|
||||||
|
}: {
|
||||||
|
series: DataSeries;
|
||||||
|
percent: number;
|
||||||
|
ascending?: boolean;
|
||||||
|
conditions?: Condition[]
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const result = analytics.percentile(series, percent, ascending, conditions);
|
||||||
|
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = handleError(error);
|
||||||
|
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Median endpoint
|
||||||
|
app.post('/api/median', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body;
|
||||||
|
const result = analytics.median(series, conditions);
|
||||||
|
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = handleError(error);
|
||||||
|
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mode endpoint
|
||||||
|
app.post('/api/mode', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body;
|
||||||
|
const result = analytics.mode(series, conditions);
|
||||||
|
res.json({ success: true, data: result } as ApiResponse<number[]>);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = handleError(error);
|
||||||
|
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Top N endpoint
|
||||||
|
app.post('/api/topn', (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
series,
|
||||||
|
n,
|
||||||
|
ascending = false,
|
||||||
|
conditions = []
|
||||||
|
}: {
|
||||||
|
series: DataSeries;
|
||||||
|
n: number;
|
||||||
|
ascending?: boolean;
|
||||||
|
conditions?: Condition[]
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const result = analytics.topN(series, n, ascending, conditions);
|
||||||
|
res.json({ success: true, data: result } as ApiResponse<number[]>);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = handleError(error);
|
||||||
|
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Max/Min endpoints
|
||||||
|
app.post('/api/max', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body;
|
||||||
|
const result = analytics.max(series, conditions);
|
||||||
|
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = handleError(error);
|
||||||
|
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/min', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body;
|
||||||
|
const result = analytics.min(series, conditions);
|
||||||
|
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = handleError(error);
|
||||||
|
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Percent change endpoint
|
||||||
|
app.post('/api/percent-change', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { series, step = 1 }: { series: DataSeries; step?: number } = req.body;
|
||||||
|
const result = analytics.percentChange(series, step);
|
||||||
|
res.json({ success: true, data: result } as ApiResponse<number[]>);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = handleError(error);
|
||||||
|
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Correlation endpoint
|
||||||
|
app.post('/api/correlation', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { series1, series2 }: { series1: DataSeries; series2: DataSeries } = req.body;
|
||||||
|
const result = analytics.correlation(series1, series2);
|
||||||
|
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = handleError(error);
|
||||||
|
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).json({ success: false, error: 'Internal server error' } as ApiResponse<any>);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Analytics API server running on port ${PORT}`);
|
||||||
|
console.log(`Health check: http://localhost:${PORT}/api/health`);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue