im-amrith/crisp
0
1import { ProcessedMarketData, PriceForecast, SeasonalPattern, MarketInfo } from '../types/market';2 3export class DataProcessor {4 private data: ProcessedMarketData[] = [];5 6 async loadData(): Promise<void> {7 try {8 // List all CSV filenames in the data directory9 const csvFiles = [10 "Almond(Badam).csv",11 "Ambada Seed.csv",12 "Antawala.csv",13 "Bamboo.csv",14 "Guava.csv",15 "Methi(Leaves).csv",16 "Papaya (Raw).csv",17 "Paddy(Dhan)(Basmati).csv",18 "Peas(Dry).csv"19 ];20 21 // Fetch and parse all CSV files in parallel22 const allDataArrays = await Promise.all(23 csvFiles.map(async (filename) => {24 const response = await fetch(`/data/${filename}`);25 const text = await response.text();26 return this.parseCSV(text);27 })28 );29 30 // Flatten the array of arrays into a single array31 this.data = allDataArrays.flat();32 } catch (error) {33 console.error('Error loading market data:', error);34 throw new Error('Failed to load market data');35 }36 }37 38 private parseCSV(csvText: string): ProcessedMarketData[] {39 const lines = csvText.split('\n');40 const headers = lines[0].split(',');41 const data: ProcessedMarketData[] = [];42 43 for (let i = 1; i < lines.length; i++) {44 const line = lines[i].trim();45 if (!line) continue;46 47 const values = this.parseCSVLine(line);48 if (values.length !== headers.length) continue;49 50 try {51 const row: ProcessedMarketData = {52 state: values[0] || '',53 district: values[1] || '',54 market: values[2] || '',55 variety: values[3] || '',56 group: values[4] || '',57 arrivals: parseFloat(values[5]) || 0,58 minPrice: parseFloat(values[6]) || 0,59 maxPrice: parseFloat(values[7]) || 0,60 modalPrice: parseFloat(values[8]) || 0,61 date: new Date(values[9] || '2023-01-01')62 };63 64 if (row.modalPrice > 0 && row.state && row.district && row.market) {65 data.push(row);66 }67 } catch (error) {68 console.warn('Error parsing row:', line, error);69 }70 }71 72 return data;73 }74 75 private parseCSVLine(line: string): string[] {76 const result: string[] = [];77 let current = '';78 let inQuotes = false;79 80 for (let i = 0; i < line.length; i++) {81 const char = line[i];82 83 if (char === '"') {84 inQuotes = !inQuotes;85 } else if (char === ',' && !inQuotes) {86 result.push(current.trim());87 current = '';88 } else {89 current += char;90 }91 }92 93 result.push(current.trim());94 return result;95 }96 97 getStates(): string[] {98 const states = new Set(this.data.map(d => d.state));99 return Array.from(states).sort();100 }101 102 getDistricts(state: string): string[] {103 const districts = new Set(104 this.data105 .filter(d => d.state === state)106 .map(d => d.district)107 );108 return Array.from(districts).sort();109 }110 111 getMarkets(state: string, district: string): string[] {112 const markets = new Set(113 this.data114 .filter(d => d.state === state && d.district === district)115 .map(d => d.market)116 );117 return Array.from(markets).sort();118 }119 120 getVarieties(): string[] {121 const varieties = new Set(this.data.map(d => d.variety));122 return Array.from(varieties).filter(v => v && v !== 'Other').sort();123 }124 125 getMarketData(state?: string, district?: string, variety?: string): ProcessedMarketData[] {126 return this.data.filter(d => {127 if (state && d.state !== state) return false;128 if (district && d.district !== district) return false;129 if (variety && d.variety !== variety) return false;130 return true;131 });132 }133 134 generatePriceForecast(variety: string, months: number = 12): PriceForecast[] {135 const varietyData = this.data.filter(d => d.variety === variety);136 if (varietyData.length === 0) return [];137 138 // Sort by date139 varietyData.sort((a, b) => a.date.getTime() - b.date.getTime());140 141 // Calculate moving average and trend142 const forecasts: PriceForecast[] = [];143 const currentDate = new Date();144 145 // Get recent price trend146 const recentData = varietyData.slice(-30); // Last 30 records147 const avgPrice = recentData.reduce((sum, d) => sum + d.modalPrice, 0) / recentData.length;148 149 // Calculate seasonal patterns150 const monthlyAvg = this.calculateMonthlyAverages(varietyData);151 152 for (let i = 0; i < months; i++) {153 const forecastDate = new Date(currentDate);154 forecastDate.setMonth(forecastDate.getMonth() + i);155 156 const month = forecastDate.getMonth();157 const seasonalMultiplier = monthlyAvg[month] / avgPrice;158 159 // Simple trend calculation with seasonal adjustment160 const trendFactor = 1 + (Math.random() - 0.5) * 0.1; // ±5% random variation161 const predictedPrice = avgPrice * seasonalMultiplier * trendFactor;162 163 forecasts.push({164 date: forecastDate.toISOString().split('T')[0],165 predictedPrice: Math.round(predictedPrice),166 confidence: Math.max(0.6, 1 - (i * 0.05)), // Decreasing confidence over time167 trend: predictedPrice > avgPrice ? 'up' : predictedPrice < avgPrice ? 'down' : 'stable'168 });169 }170 171 return forecasts;172 }173 174 private calculateMonthlyAverages(data: ProcessedMarketData[]): number[] {175 const monthlyData: { [key: number]: number[] } = {};176 177 data.forEach(d => {178 const month = d.date.getMonth();179 if (!monthlyData[month]) monthlyData[month] = [];180 monthlyData[month].push(d.modalPrice);181 });182 183 const monthlyAvg: number[] = [];184 for (let i = 0; i < 12; i++) {185 if (monthlyData[i] && monthlyData[i].length > 0) {186 monthlyAvg[i] = monthlyData[i].reduce((sum, price) => sum + price, 0) / monthlyData[i].length;187 } else {188 // Use overall average if no data for this month189 const overallAvg = data.reduce((sum, d) => sum + d.modalPrice, 0) / data.length;190 monthlyAvg[i] = overallAvg;191 }192 }193 194 return monthlyAvg;195 }196 197 private calculateMonthlyPriceAverages(data: ProcessedMarketData[], priceType: 'minPrice' | 'maxPrice' | 'modalPrice'): { month: string, avg: number }[] {198 const monthlyData: { [key: number]: number[] } = {};199 200 data.forEach(d => {201 const month = d.date.getMonth();202 if (!monthlyData[month]) monthlyData[month] = [];203 const price = priceType === 'minPrice' ? d.minPrice : priceType === 'maxPrice' ? d.maxPrice : d.modalPrice;204 if (price > 0) { // Only include valid prices205 monthlyData[month].push(price);206 }207 });208 209 const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];210 211 const overallAvg = data.length > 0 212 ? data.reduce((sum, d) => {213 const price = priceType === 'minPrice' ? d.minPrice : priceType === 'maxPrice' ? d.maxPrice : d.modalPrice;214 return sum + price;215 }, 0) / data.filter(d => (priceType === 'minPrice' ? d.minPrice : priceType === 'maxPrice' ? d.maxPrice : d.modalPrice) > 0).length216 : 0;217 218 const monthlyAvgs: { month: string, avg: number }[] = [];219 for (let i = 0; i < 12; i++) {220 let avgForMonth: number;221 if (monthlyData[i] && monthlyData[i].length > 0) {222 avgForMonth = monthlyData[i].reduce((sum, price) => sum + price, 0) / monthlyData[i].length;223 } else {224 avgForMonth = overallAvg; // Fallback to overall average225 }226 monthlyAvgs.push({ month: months[i], avg: avgForMonth });227 }228 229 return monthlyAvgs;230 }231 232 getSeasonalPatterns(variety: string): SeasonalPattern[] {233 const varietyData = this.data.filter(d => d.variety === variety);234 if (varietyData.length === 0) return [];235 236 const monthlyAvg = this.calculateMonthlyAverages(varietyData);237 const overallAvg = monthlyAvg.reduce((sum, price) => sum + price, 0) / 12;238 239 const months = [240 'January', 'February', 'March', 'April', 'May', 'June',241 'July', 'August', 'September', 'October', 'November', 'December'242 ];243 244 return months.map((month, index) => {245 const priceIndex = monthlyAvg[index] / overallAvg;246 let recommendation: 'excellent' | 'good' | 'average' | 'poor';247 248 if (priceIndex >= 1.15) recommendation = 'excellent';249 else if (priceIndex >= 1.05) recommendation = 'good';250 else if (priceIndex >= 0.95) recommendation = 'average';251 else recommendation = 'poor';252 253 return {254 month,255 averagePrice: Math.round(monthlyAvg[index]),256 priceIndex,257 recommendation258 };259 });260 }261 262 getBestMarkets(variety: string, userState?: string, userMarket?: string, limit: number = 5): MarketInfo[] {263 let varietyData = this.data.filter(d => d.variety === variety);264 265 // Filter by market if one is provided266 if (userMarket) {267 varietyData = varietyData.filter(d => d.market === userMarket);268 }269 270 // Group by market271 const marketGroups: { [key: string]: ProcessedMarketData[] } = {};272 273 varietyData.forEach(d => {274 const key = `${d.state}-${d.district}-${d.market}`;275 if (!marketGroups[key]) marketGroups[key] = [];276 marketGroups[key].push(d);277 });278 279 const marketAverages = Object.entries(marketGroups).map(([key, data]) => {280 if (data.length === 0) return null;281 282 // Find highest and lowest prices from monthly averages283 const monthlyMaxPrices = this.calculateMonthlyPriceAverages(data, 'maxPrice');284 const monthlyMinPrices = this.calculateMonthlyPriceAverages(data, 'minPrice');285 286 const highPriceEntry = monthlyMaxPrices.reduce((max, p) => p.avg > max.avg ? p : max, { avg: 0, month: 'N/A' });287 const lowPriceEntry = monthlyMinPrices.reduce((min, p) => (p.avg < min.avg && p.avg > 0) ? p : min, { avg: Infinity, month: 'N/A' });288 289 // The main price for sorting and display will be the historical high price average290 const displayPrice = highPriceEntry.avg;291 292 return {293 ...data.sort((a, b) => b.date.getTime() - a.date.getTime())[0], // Use latest for base info294 modalPrice: displayPrice,295 highPrice: Math.round(highPriceEntry.avg),296 highPriceMonth: highPriceEntry.month,297 lowPrice: lowPriceEntry.avg === Infinity ? 0 : Math.round(lowPriceEntry.avg),298 lowPriceMonth: lowPriceEntry.month,299 arrivals: data.reduce((sum, d) => sum + d.arrivals, 0) / data.length300 };301 }).filter((m): m is MarketInfo => m !== null && m.highPrice > 0);302 303 // Sort by high price (descending) and prioritize user's state304 marketAverages.sort((a, b) => {305 if (userState) {306 if (a.state === userState && b.state !== userState) return -1;307 if (b.state === userState && a.state !== userState) return 1;308 }309 return (b.highPrice || 0) - (a.highPrice || 0);310 });311 312 return marketAverages.slice(0, limit);313 }314 315 getBestStateMarkets(variety: string, state: string, limit: number = 3): MarketInfo[] {316 const stateVarietyData = this.data.filter(d => d.variety === variety && d.state === state);317 318 const marketGroups: { [key: string]: ProcessedMarketData[] } = {};319 320 stateVarietyData.forEach(d => {321 const key = `${d.state}-${d.district}-${d.market}`;322 if (!marketGroups[key]) marketGroups[key] = [];323 marketGroups[key].push(d);324 });325 326 const marketAverages = Object.entries(marketGroups).map(([key, data]) => {327 if (data.length === 0) return null;328 329 const monthlyMaxPrices = this.calculateMonthlyPriceAverages(data, 'maxPrice');330 const monthlyMinPrices = this.calculateMonthlyPriceAverages(data, 'minPrice');331 332 const highPriceEntry = monthlyMaxPrices.reduce((max, p) => p.avg > max.avg ? p : max, { avg: 0, month: 'N/A' });333 const lowPriceEntry = monthlyMinPrices.reduce((min, p) => (p.avg < min.avg && p.avg > 0) ? p : min, { avg: Infinity, month: 'N/A' });334 335 const displayPrice = highPriceEntry.avg;336 337 return {338 ...data.sort((a, b) => b.date.getTime() - a.date.getTime())[0],339 modalPrice: displayPrice,340 highPrice: Math.round(highPriceEntry.avg),341 highPriceMonth: highPriceEntry.month,342 lowPrice: lowPriceEntry.avg === Infinity ? 0 : Math.round(lowPriceEntry.avg),343 lowPriceMonth: lowPriceEntry.month,344 arrivals: data.reduce((sum, d) => sum + d.arrivals, 0) / data.length345 };346 }).filter((m): m is MarketInfo => m !== null && m.highPrice > 0);347 348 return marketAverages349 .sort((a, b) => (b.highPrice || 0) - (a.highPrice || 0))350 .slice(0, limit);351 }352}