iris-meteo 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -0
- package/index.js +585 -0
- package/package.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Iris Weather CLI
|
|
2
|
+
|
|
3
|
+
A command-line tool for correlating any CSV data with historical weather data from Open-Meteo.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **CSV-Weather Correlation**: Analyze relationships between any CSV data and weather conditions
|
|
8
|
+
- **Historical Data**: Use Open-Meteo archive endpoint for past dates
|
|
9
|
+
- **Flexible Filtering**: Filter both CSV and weather data
|
|
10
|
+
- **Comprehensive Statistics**: Pearson correlation, R-squared, and detailed analysis
|
|
11
|
+
- **Multiple Weather Parameters**: Temperature, precipitation, wind speed, etc.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install iris-meteo -g
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Refer to the open-meteo docs for how to install and run a local instance: [https://github.com/open-meteo/open-meteo/blob/main/docs/getting-started.md](https://github.com/open-meteo/open-meteo/blob/main/docs/getting-started.md)
|
|
20
|
+
|
|
21
|
+
## Basic Usage
|
|
22
|
+
|
|
23
|
+
### Correlate data with temperature
|
|
24
|
+
```bash
|
|
25
|
+
iris --csv data.csv --latitude 52.52 --longitude 13.41
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Filter data before correlation
|
|
31
|
+
```bash
|
|
32
|
+
iris --csv data.csv --latitude 52.52 --longitude 13.41 \
|
|
33
|
+
--filter "amount > 1000" --weather-filter "temperature_2m > 15"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Custom field names and parameters
|
|
37
|
+
```bash
|
|
38
|
+
iris --csv energy_data.csv --csv-date "timestamp" --csv-value "kwh_used" \
|
|
39
|
+
--weather-param precipitation --latitude 51.5074 --longitude -0.1278
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Usage: iris [options]
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
-V, --version output the version number
|
|
49
|
+
--csv <file> Load weather data from CSV file
|
|
50
|
+
--filter <expression> Filter CSV data using expressions like "temperature_2m:15" or "temperature_2m > 20"
|
|
51
|
+
--api-url <url> Open-Meteo API URL (for local instances) (default: "http://localhost:8080/v1/forecast")
|
|
52
|
+
--latitude <lat> Latitude for API requests
|
|
53
|
+
--longitude <lon> Longitude for API requests
|
|
54
|
+
--parameter <param> Weather parameter to analyze (e.g., temperature_2m, precipitation) (default: "temperature_2m")
|
|
55
|
+
--output <file> Output file for results
|
|
56
|
+
--verbose Show detailed output (default: false)
|
|
57
|
+
--o <filter> Filter API response data using expressions like "temperature_2m > 20" or "precipitation <= 5"
|
|
58
|
+
--correlate Analyze correlation between CSV and API data (default: false)
|
|
59
|
+
-h, --help display help for command
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### Supported Date Formats
|
|
64
|
+
The tool supports multiple date formats for international compatibility:
|
|
65
|
+
|
|
66
|
+
- **ISO Format**: `YYYY-MM-DD` (e.g., `2023-01-15`)
|
|
67
|
+
- **European Format**: `DD.MM.YYYY` (e.g., `15.01.2023`)
|
|
68
|
+
- **US Format**: `MM/DD/YYYY` (e.g., `01/15/2023`)
|
|
69
|
+
- **ISO with Time**: `YYYY-MM-DDTHH:MM` (e.g., `2023-01-15T14:30`)
|
|
70
|
+
|
|
71
|
+
The tool will automatically detect and normalize all supported date formats to match with weather data.
|
|
72
|
+
|
|
73
|
+
## Requirements
|
|
74
|
+
|
|
75
|
+
- Node.js 12+
|
|
76
|
+
- Local Open-Meteo instance (for API features)
|
|
77
|
+
- Internet connection (only if using public Open-Meteo API)
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
ISC
|
package/index.js
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const csv = require('csv-parser');
|
|
6
|
+
const axios = require('axios');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('iris')
|
|
13
|
+
.description('CLI tool for correlating any CSV data with weather data from open-meteo.com')
|
|
14
|
+
.version('1.0.0');
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.option('--csv <file>', 'Load data from CSV file (must contain date column)')
|
|
18
|
+
.option('--csv-date <field>', 'CSV date field name', 'date')
|
|
19
|
+
.option('--csv-value <field>', 'CSV value field to correlate with weather', 'sales_amount')
|
|
20
|
+
.option('--weather-param <param>', 'Daily weather parameter to correlate with (e.g., temperature_2m_max, precipitation_sum, windspeed_10m_max)', 'temperature_2m_max')
|
|
21
|
+
.option('--api-url <url>', 'Open-Meteo API URL (for local instances)', 'http://localhost:8080/v1/forecast')
|
|
22
|
+
.option('--latitude <lat>', 'Latitude for weather data')
|
|
23
|
+
.option('--longitude <lon>', 'Longitude for weather data')
|
|
24
|
+
.option('--start-date <date>', 'Start date for analysis (YYYY-MM-DD)')
|
|
25
|
+
.option('--end-date <date>', 'End date for analysis (YYYY-MM-DD)')
|
|
26
|
+
.option('--filter <expression>', 'Filter CSV data using expressions like "sales_amount > 1000"')
|
|
27
|
+
.option('--weather-filter <expression>', 'Filter weather data using expressions like "temperature_2m > 15"')
|
|
28
|
+
.option('--output <file>', 'Output file for correlation results')
|
|
29
|
+
.option('--verbose', 'Show detailed correlation analysis', false);
|
|
30
|
+
|
|
31
|
+
program.parse(process.argv);
|
|
32
|
+
|
|
33
|
+
const options = program.opts();
|
|
34
|
+
|
|
35
|
+
// Main function to handle the CLI logic
|
|
36
|
+
async function main() {
|
|
37
|
+
try {
|
|
38
|
+
// Validate required options
|
|
39
|
+
if (!options.csv) {
|
|
40
|
+
console.error('Error: --csv option is required');
|
|
41
|
+
program.help();
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!options.latitude || !options.longitude) {
|
|
46
|
+
console.error('Error: Both --latitude and --longitude are required');
|
|
47
|
+
program.help();
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log('Iris Weather Correlation Analysis');
|
|
52
|
+
console.log('=================================');
|
|
53
|
+
|
|
54
|
+
// Load CSV data
|
|
55
|
+
const csvData = await loadAndProcessCsvData(options);
|
|
56
|
+
console.log(`Loaded ${csvData.length} records from CSV file`);
|
|
57
|
+
|
|
58
|
+
// Apply CSV filtering if specified
|
|
59
|
+
let filteredCsvData = csvData;
|
|
60
|
+
if (options.filter) {
|
|
61
|
+
filteredCsvData = filterCsvData(csvData, options.filter);
|
|
62
|
+
console.log(`🔍 Filtered CSV to ${filteredCsvData.length} records`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Determine date range for weather data
|
|
66
|
+
const dateRange = determineDateRange(filteredCsvData, options);
|
|
67
|
+
console.log(`Analyzing date range: ${dateRange.start} to ${dateRange.end}`);
|
|
68
|
+
|
|
69
|
+
// Fetch weather data
|
|
70
|
+
const weatherData = await fetchWeatherData(options, dateRange);
|
|
71
|
+
console.log(`Fetched ${weatherData.length} weather records from Open-Meteo API`);
|
|
72
|
+
|
|
73
|
+
// Apply weather filtering if specified
|
|
74
|
+
let filteredWeatherData = weatherData;
|
|
75
|
+
if (options.weatherFilter) {
|
|
76
|
+
filteredWeatherData = filterWeatherData(weatherData, options.weatherFilter);
|
|
77
|
+
console.log(`🌡️ Filtered weather data to ${filteredWeatherData.length} records`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Match CSV data with weather data by date
|
|
81
|
+
const matchedData = matchDataByDate(filteredCsvData, filteredWeatherData, options);
|
|
82
|
+
console.log(`Found ${matchedData.length} matching date pairs`);
|
|
83
|
+
|
|
84
|
+
if (matchedData.length < 2) {
|
|
85
|
+
console.warn('⚠️ Insufficient data for meaningful correlation analysis');
|
|
86
|
+
console.warn(' Try adjusting your date range or filters');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Perform correlation analysis
|
|
91
|
+
const correlationResults = analyzeCorrelation(matchedData, options);
|
|
92
|
+
displayCorrelationResults(correlationResults, options);
|
|
93
|
+
|
|
94
|
+
// Save results if requested
|
|
95
|
+
if (options.output) {
|
|
96
|
+
saveResultsToFile(correlationResults, options.output);
|
|
97
|
+
console.log(`💾 Results saved to ${options.output}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('❌ Error:', error.message);
|
|
102
|
+
if (options.verbose) {
|
|
103
|
+
console.error(error.stack);
|
|
104
|
+
}
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Load and process CSV data
|
|
110
|
+
function loadAndProcessCsvData(options) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
if (!fs.existsSync(options.csv)) {
|
|
113
|
+
reject(new Error(`CSV file not found: ${options.csv}`));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const results = [];
|
|
118
|
+
fs.createReadStream(options.csv)
|
|
119
|
+
.pipe(csv())
|
|
120
|
+
.on('data', (data) => results.push(data))
|
|
121
|
+
.on('end', () => {
|
|
122
|
+
if (results.length === 0) {
|
|
123
|
+
reject(new Error('CSV file is empty or contains no valid data'));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Validate that required fields exist
|
|
127
|
+
if (!results[0].hasOwnProperty(options.csvDate)) {
|
|
128
|
+
reject(new Error(`CSV file does not contain a '${options.csvDate}' field`));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!results[0].hasOwnProperty(options.csvValue)) {
|
|
132
|
+
reject(new Error(`CSV file does not contain a '${options.csvValue}' field`));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
resolve(results);
|
|
136
|
+
})
|
|
137
|
+
.on('error', (error) => reject(new Error(`Error reading CSV file: ${error.message}`)));
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Determine date range for analysis
|
|
142
|
+
function determineDateRange(csvData, options) {
|
|
143
|
+
// Parse and normalize dates from CSV data
|
|
144
|
+
const dateObjects = [];
|
|
145
|
+
const validDates = [];
|
|
146
|
+
|
|
147
|
+
csvData.forEach(item => {
|
|
148
|
+
let dateStr = item[options.csvDate];
|
|
149
|
+
|
|
150
|
+
// Remove time portion if present
|
|
151
|
+
if (dateStr.includes('T')) {
|
|
152
|
+
dateStr = dateStr.split('T')[0];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Try to parse the date in various formats
|
|
156
|
+
const parsedDate = parseFlexibleDate(dateStr);
|
|
157
|
+
if (parsedDate) {
|
|
158
|
+
dateObjects.push(parsedDate);
|
|
159
|
+
validDates.push(dateStr);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (dateObjects.length === 0) {
|
|
164
|
+
throw new Error('No valid dates found in CSV data. Supported formats: YYYY-MM-DD, DD.MM.YYYY, MM/DD/YYYY');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Sort dates to find min and max
|
|
168
|
+
dateObjects.sort((a, b) => a - b);
|
|
169
|
+
|
|
170
|
+
// Format dates back to YYYY-MM-DD for API
|
|
171
|
+
const formatDate = (date) => {
|
|
172
|
+
return date.toISOString().split('T')[0];
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const startDate = options.startDate || formatDate(dateObjects[0]);
|
|
176
|
+
const endDate = options.endDate || formatDate(dateObjects[dateObjects.length - 1]);
|
|
177
|
+
|
|
178
|
+
return { start: startDate, end: endDate };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Parse dates in multiple formats
|
|
182
|
+
function parseFlexibleDate(dateStr) {
|
|
183
|
+
// Try ISO format first (YYYY-MM-DD)
|
|
184
|
+
if (dateStr.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
|
185
|
+
const date = new Date(dateStr);
|
|
186
|
+
if (!isNaN(date.getTime())) return date;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Try European format (DD.MM.YYYY)
|
|
190
|
+
if (dateStr.match(/^\d{2}\.\d{2}\.\d{4}$/)) {
|
|
191
|
+
const parts = dateStr.split('.');
|
|
192
|
+
const date = new Date(`${parts[2]}-${parts[1]}-${parts[0]}`);
|
|
193
|
+
if (!isNaN(date.getTime())) return date;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Try US format (MM/DD/YYYY)
|
|
197
|
+
if (dateStr.match(/^\d{2}\/\d{2}\/\d{4}$/)) {
|
|
198
|
+
const parts = dateStr.split('/');
|
|
199
|
+
const date = new Date(`${parts[2]}-${parts[0]}-${parts[1]}`);
|
|
200
|
+
if (!isNaN(date.getTime())) return date;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Try ISO with time (YYYY-MM-DDTHH:MM)
|
|
204
|
+
if (dateStr.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/)) {
|
|
205
|
+
const date = new Date(dateStr);
|
|
206
|
+
if (!isNaN(date.getTime())) return date;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Fetch weather data from Open-Meteo API
|
|
213
|
+
async function fetchWeatherData(options, dateRange) {
|
|
214
|
+
try {
|
|
215
|
+
let apiUrl = options.apiUrl;
|
|
216
|
+
|
|
217
|
+
// Map old hourly parameter names to their daily equivalents
|
|
218
|
+
const parameterMapping = {
|
|
219
|
+
'temperature_2m': 'temperature_2m_max',
|
|
220
|
+
'temperature': 'temperature_2m_max',
|
|
221
|
+
'temp': 'temperature_2m_max',
|
|
222
|
+
'precipitation': 'precipitation_sum',
|
|
223
|
+
'rain': 'rain_sum',
|
|
224
|
+
'windspeed': 'windspeed_10m_max',
|
|
225
|
+
'wind': 'windspeed_10m_max'
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Apply parameter mapping if needed
|
|
229
|
+
let weatherParam = options.weatherParam;
|
|
230
|
+
if (parameterMapping[weatherParam]) {
|
|
231
|
+
console.log(`🔄 Mapping parameter '${weatherParam}' to daily equivalent '${parameterMapping[weatherParam]}'`);
|
|
232
|
+
weatherParam = parameterMapping[weatherParam];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const params = {
|
|
236
|
+
latitude: options.latitude,
|
|
237
|
+
longitude: options.longitude,
|
|
238
|
+
daily: weatherParam,
|
|
239
|
+
start_date: dateRange.start,
|
|
240
|
+
end_date: dateRange.end,
|
|
241
|
+
timezone: 'auto',
|
|
242
|
+
models: 'ecmwf_ifs025'
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
console.log(`Connecting to Open-Meteo API at: ${apiUrl}`);
|
|
246
|
+
|
|
247
|
+
const response = await axios.get(apiUrl, { params });
|
|
248
|
+
|
|
249
|
+
if (!response.data || !response.data.daily) {
|
|
250
|
+
throw new Error('Invalid API response format - expected daily weather data');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const dailyData = response.data.daily;
|
|
254
|
+
const timeData = dailyData.time;
|
|
255
|
+
const parameterData = dailyData[weatherParam];
|
|
256
|
+
|
|
257
|
+
if (!timeData || !parameterData || timeData.length !== parameterData.length) {
|
|
258
|
+
throw new Error('Inconsistent data in API response');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Convert to date-value pairs for daily data
|
|
262
|
+
const results = [];
|
|
263
|
+
for (let i = 0; i < timeData.length; i++) {
|
|
264
|
+
results.push({
|
|
265
|
+
date: timeData[i],
|
|
266
|
+
[options.weatherParam]: parameterData[i] // Use original parameter name for consistency
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return results;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
throw new Error(`Failed to fetch weather data: ${error.message}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
// Filter CSV data
|
|
279
|
+
function filterCsvData(data, filterExpression) {
|
|
280
|
+
const [field, operator, value] = parseFilterExpression(filterExpression);
|
|
281
|
+
|
|
282
|
+
return data.filter(item => {
|
|
283
|
+
const itemValue = parseFloat(item[field]);
|
|
284
|
+
const filterValue = parseFloat(value);
|
|
285
|
+
|
|
286
|
+
if (isNaN(itemValue) || isNaN(filterValue)) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
switch (operator) {
|
|
291
|
+
case '=':
|
|
292
|
+
case ':':
|
|
293
|
+
return itemValue === filterValue;
|
|
294
|
+
case '>':
|
|
295
|
+
return itemValue > filterValue;
|
|
296
|
+
case '<':
|
|
297
|
+
return itemValue < filterValue;
|
|
298
|
+
case '>=':
|
|
299
|
+
return itemValue >= filterValue;
|
|
300
|
+
case '<=':
|
|
301
|
+
return itemValue <= filterValue;
|
|
302
|
+
case '!=':
|
|
303
|
+
return itemValue !== filterValue;
|
|
304
|
+
default:
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Filter weather data
|
|
311
|
+
function filterWeatherData(data, filterExpression) {
|
|
312
|
+
const [field, operator, value] = parseFilterExpression(filterExpression);
|
|
313
|
+
|
|
314
|
+
return data.filter(item => {
|
|
315
|
+
const itemValue = parseFloat(item[field]);
|
|
316
|
+
const filterValue = parseFloat(value);
|
|
317
|
+
|
|
318
|
+
if (isNaN(itemValue) || isNaN(filterValue)) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
switch (operator) {
|
|
323
|
+
case '=':
|
|
324
|
+
case ':':
|
|
325
|
+
return itemValue === filterValue;
|
|
326
|
+
case '>':
|
|
327
|
+
return itemValue > filterValue;
|
|
328
|
+
case '<':
|
|
329
|
+
return itemValue < filterValue;
|
|
330
|
+
case '>=':
|
|
331
|
+
return itemValue >= filterValue;
|
|
332
|
+
case '<=':
|
|
333
|
+
return itemValue <= filterValue;
|
|
334
|
+
case '!=':
|
|
335
|
+
return itemValue !== filterValue;
|
|
336
|
+
default:
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Helper function to parse numbers in German format (commas as thousand separators, periods as decimals)
|
|
343
|
+
function parseGermanNumber(value) {
|
|
344
|
+
if (typeof value !== 'string') {
|
|
345
|
+
return parseFloat(value);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Remove thousand separators (commas) and replace decimal points with dots
|
|
349
|
+
const cleanedValue = value.replace(/\./g, '').replace(/,/g, '.');
|
|
350
|
+
return parseFloat(cleanedValue);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Match CSV data with weather data by date
|
|
354
|
+
function matchDataByDate(csvData, weatherData, options) {
|
|
355
|
+
const matchedPairs = [];
|
|
356
|
+
|
|
357
|
+
// Create a map of weather data by date for faster lookup
|
|
358
|
+
const weatherMap = {};
|
|
359
|
+
weatherData.forEach(weatherItem => {
|
|
360
|
+
const date = weatherItem.date;
|
|
361
|
+
if (!weatherMap[date]) {
|
|
362
|
+
weatherMap[date] = [];
|
|
363
|
+
}
|
|
364
|
+
weatherMap[date].push(weatherItem);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Match each CSV record with corresponding weather data
|
|
368
|
+
csvData.forEach(csvItem => {
|
|
369
|
+
// Extract and normalize date
|
|
370
|
+
let dateStr = csvItem[options.csvDate];
|
|
371
|
+
|
|
372
|
+
// Parse the CSV date to get the normalized format (YYYY-MM-DD)
|
|
373
|
+
const parsedDate = parseFlexibleDate(dateStr);
|
|
374
|
+
if (!parsedDate) {
|
|
375
|
+
console.warn(`⚠️ Skipping record with unparseable date: ${dateStr}`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Format as YYYY-MM-DD for matching
|
|
380
|
+
const normalizedDate = parsedDate.toISOString().split('T')[0];
|
|
381
|
+
|
|
382
|
+
// Find matching weather data
|
|
383
|
+
if (weatherMap[normalizedDate] && weatherMap[normalizedDate].length > 0) {
|
|
384
|
+
// Use the first weather record for the day (could be enhanced to use average, etc.)
|
|
385
|
+
const weatherItem = weatherMap[normalizedDate][0];
|
|
386
|
+
|
|
387
|
+
const csvValue = parseGermanNumber(csvItem[options.csvValue]);
|
|
388
|
+
const weatherValue = parseFloat(weatherItem[options.weatherParam]);
|
|
389
|
+
|
|
390
|
+
// Debug logging removed
|
|
391
|
+
|
|
392
|
+
matchedPairs.push({
|
|
393
|
+
date: normalizedDate,
|
|
394
|
+
originalDate: dateStr,
|
|
395
|
+
csvValue: csvValue,
|
|
396
|
+
weatherValue: weatherValue,
|
|
397
|
+
csvData: csvItem,
|
|
398
|
+
weatherData: weatherItem
|
|
399
|
+
});
|
|
400
|
+
} else {
|
|
401
|
+
console.warn(`⚠️ No weather data found for date: ${normalizedDate} (original: ${dateStr})`);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return matchedPairs;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Parse filter expression
|
|
409
|
+
function parseFilterExpression(expression) {
|
|
410
|
+
// Handle equality operators (= or :)
|
|
411
|
+
const equalityMatch = expression.match(/^(\w+)\s*(=|:)\s*(\S+)$/);
|
|
412
|
+
if (equalityMatch) {
|
|
413
|
+
return [equalityMatch[1], '=', equalityMatch[3]];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Handle comparison operators
|
|
417
|
+
const comparisonMatch = expression.match(/^(\w+)\s*(>=|<=|>|<|!=)\s*(\S+)$/);
|
|
418
|
+
if (comparisonMatch) {
|
|
419
|
+
return [comparisonMatch[1], comparisonMatch[2], comparisonMatch[3]];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
throw new Error(`Invalid filter expression: ${expression}. Use format like "field:value" or "field > value"`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Analyze correlation between CSV values and weather data
|
|
426
|
+
function analyzeCorrelation(matchedData, options) {
|
|
427
|
+
// Extract values
|
|
428
|
+
const csvValues = [];
|
|
429
|
+
const weatherValues = [];
|
|
430
|
+
|
|
431
|
+
matchedData.forEach(pair => {
|
|
432
|
+
if (!isNaN(pair.csvValue) && !isNaN(pair.weatherValue)) {
|
|
433
|
+
csvValues.push(pair.csvValue);
|
|
434
|
+
weatherValues.push(pair.weatherValue);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// Debug logging removed
|
|
439
|
+
|
|
440
|
+
if (csvValues.length < 2) {
|
|
441
|
+
return {
|
|
442
|
+
csvField: options.csvValue,
|
|
443
|
+
weatherParam: options.weatherParam,
|
|
444
|
+
dataPoints: csvValues.length,
|
|
445
|
+
correlation: null,
|
|
446
|
+
rSquared: null,
|
|
447
|
+
message: 'Insufficient matching data points for correlation analysis'
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Calculate Pearson correlation coefficient
|
|
452
|
+
const correlation = calculatePearsonCorrelation(csvValues, weatherValues);
|
|
453
|
+
const rSquared = Math.pow(correlation, 2);
|
|
454
|
+
|
|
455
|
+
// Calculate additional statistics
|
|
456
|
+
const csvStats = calculateBasicStats(csvValues);
|
|
457
|
+
const weatherStats = calculateBasicStats(weatherValues);
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
csvField: options.csvValue,
|
|
461
|
+
weatherParam: options.weatherParam,
|
|
462
|
+
dataPoints: csvValues.length,
|
|
463
|
+
correlation: correlation,
|
|
464
|
+
rSquared: rSquared,
|
|
465
|
+
interpretation: interpretCorrelation(correlation),
|
|
466
|
+
csvStats: csvStats,
|
|
467
|
+
weatherStats: weatherStats,
|
|
468
|
+
matchedData: matchedData
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Calculate basic statistics
|
|
473
|
+
function calculateBasicStats(values) {
|
|
474
|
+
if (values.length === 0) {
|
|
475
|
+
return {
|
|
476
|
+
min: null,
|
|
477
|
+
max: null,
|
|
478
|
+
average: null,
|
|
479
|
+
median: null
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const sortedValues = [...values].sort((a, b) => a - b);
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
min: Math.min(...values),
|
|
487
|
+
max: Math.max(...values),
|
|
488
|
+
average: values.reduce((sum, val) => sum + val, 0) / values.length,
|
|
489
|
+
median: calculateMedian(sortedValues)
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Calculate median value
|
|
494
|
+
function calculateMedian(sortedValues) {
|
|
495
|
+
const middle = Math.floor(sortedValues.length / 2);
|
|
496
|
+
|
|
497
|
+
if (sortedValues.length % 2 === 0) {
|
|
498
|
+
return (sortedValues[middle - 1] + sortedValues[middle]) / 2;
|
|
499
|
+
} else {
|
|
500
|
+
return sortedValues[middle];
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Calculate Pearson correlation coefficient
|
|
505
|
+
function calculatePearsonCorrelation(x, y) {
|
|
506
|
+
const n = x.length;
|
|
507
|
+
|
|
508
|
+
// Calculate means
|
|
509
|
+
const meanX = x.reduce((sum, val) => sum + val, 0) / n;
|
|
510
|
+
const meanY = y.reduce((sum, val) => sum + val, 0) / n;
|
|
511
|
+
|
|
512
|
+
// Calculate covariance and standard deviations
|
|
513
|
+
let covariance = 0;
|
|
514
|
+
let stdDevX = 0;
|
|
515
|
+
let stdDevY = 0;
|
|
516
|
+
|
|
517
|
+
for (let i = 0; i < n; i++) {
|
|
518
|
+
const diffX = x[i] - meanX;
|
|
519
|
+
const diffY = y[i] - meanY;
|
|
520
|
+
covariance += diffX * diffY;
|
|
521
|
+
stdDevX += Math.pow(diffX, 2);
|
|
522
|
+
stdDevY += Math.pow(diffY, 2);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Handle division by zero
|
|
526
|
+
if (stdDevX === 0 || stdDevY === 0) {
|
|
527
|
+
return 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return covariance / (Math.sqrt(stdDevX) * Math.sqrt(stdDevY));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Interpret correlation coefficient
|
|
534
|
+
function interpretCorrelation(r) {
|
|
535
|
+
const absR = Math.abs(r);
|
|
536
|
+
|
|
537
|
+
if (absR >= 0.9) return 'Very strong correlation';
|
|
538
|
+
if (absR >= 0.7) return 'Strong correlation';
|
|
539
|
+
if (absR >= 0.5) return 'Moderate correlation';
|
|
540
|
+
if (absR >= 0.3) return 'Weak correlation';
|
|
541
|
+
return 'No or negligible correlation';
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Display correlation results
|
|
545
|
+
function displayCorrelationResults(results, options) {
|
|
546
|
+
console.log('\nCorrelation Analysis Results');
|
|
547
|
+
console.log('============================');
|
|
548
|
+
console.log(`CSV Field: ${results.csvField}`);
|
|
549
|
+
console.log(`Weather Parameter: ${results.weatherParam}`);
|
|
550
|
+
console.log(`Data Points Analyzed: ${results.dataPoints}`);
|
|
551
|
+
|
|
552
|
+
if (results.correlation !== null) {
|
|
553
|
+
console.log(`\nStatistical Results:`);
|
|
554
|
+
console.log(` Pearson Correlation (r): ${results.correlation.toFixed(4)}`);
|
|
555
|
+
console.log(` R-squared: ${results.rSquared.toFixed(4)}`);
|
|
556
|
+
console.log(` Interpretation: ${results.interpretation}`);
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
} else {
|
|
560
|
+
console.log(`${results.message}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Save results to file
|
|
565
|
+
function saveResultsToFile(results, filePath) {
|
|
566
|
+
const output = {
|
|
567
|
+
timestamp: new Date().toISOString(),
|
|
568
|
+
analysisType: 'CSV-Weather Correlation',
|
|
569
|
+
correlationResults: {
|
|
570
|
+
csvField: results.csvField,
|
|
571
|
+
weatherParam: results.weatherParam,
|
|
572
|
+
dataPoints: results.dataPoints,
|
|
573
|
+
correlation: results.correlation,
|
|
574
|
+
rSquared: results.rSquared,
|
|
575
|
+
interpretation: results.interpretation,
|
|
576
|
+
csvStats: results.csvStats,
|
|
577
|
+
weatherStats: results.weatherStats
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
fs.writeFileSync(filePath, JSON.stringify(output, null, 2), 'utf8');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Start the application
|
|
585
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "iris-meteo",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI tool for analyzing weather data with open-meteo.com",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"iris": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
11
|
+
"start": "node index.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["weather", "cli", "open-meteo", "analysis"],
|
|
14
|
+
"author": "",
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"type": "commonjs",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"axios": "^1.13.5",
|
|
19
|
+
"commander": "^14.0.3",
|
|
20
|
+
"csv-parser": "^3.2.0"
|
|
21
|
+
}
|
|
22
|
+
}
|