0

I have over 3000 files that need to be combined based on specific attributes in the files. For example, each file is named by its ID (Eg. 101567AD_Mly.txt).These IDs correspond to latitude and longitude values that I will concatenate to the file after. I need to add a leading column in each output file with the ID. I would then like to merge lines in the input files so that the output looks like IDENTIFICATION, YEAR, JANUARY MONTH CODE (1), JANUARY PRECIPITATION, FEBRUARY MONTH CODE (2), FEBRUARY PRECIPITATION, MARCH MONTH CODE (3), MARCH PRECIPITATION......
Currently, the files are separating each month into a new line like the example below where it is currently structured as YEAR, MONTH, UNKNOWN VALUE, PRECIPITATION AMOUNT.I would like to combine the 3000+ files based on the ID for each, which is their filename. Ideally, I would have a file for each year of data (1979,1980, 1981, etc) so that all files with those years of data (eg. 1981) would all be in one file with each line representing a different ID. The filenames describe the identification of where this data came from so I would like it as the leading column in each file. Here is an example of what the files look like:

YEAR, MONTH, REMOVE THIRD COLUMN, PRECIPITATION 
1980 1 0 112.106
1980 2 0 131.909
1980 3 0 58.842
1980 4 0 42.075
1980 5 0 45.268
1980 6 0 126.168
1980 7 0 30.159
1980 8 0 32.168
1980 9 0 39.48
1980 10 0 46.117
1980 11 0 234.089
1980 12 0 180.227
1981 1 0 62.795
1981 2 0 131.817
1981 3 0 73.429
1981 4 0 77.282
1981 5 0 54.224
1981 6 0 81.217
1981 7 0 18.469
1981 8 0 12.695
1981 9 0 83.4
1981 10 0 130.957
1981 11 0 151.07
1981 12 0 190.582
1982 1 0 210.604
1982 2 0 143.732
1982 3 0 26.124
1982 4 0 45.952
1982 5 0 10.38
1982 6 0 33.602
1982 7 0 42.218
1982 8 0 9.541
1982 9 0 39.356
1982 10 0 87.037

The third column can just be removed. Any help to combine all these using perl or python would be great. Either a one-liner or script. I am using linux to do this.

The guy who used to do this work and based everything around Perl has retired leaving a lot of gaps in the knowledge transfer. I am struggling to piece some of it together.

The output format should look like:

IDENTIFICATION, YEAR, MONTH CODE, PRECIPITATION FOR THAT MONTH
ADB103884_Mly, 1989, 1, 123.56, 2, 56.23, 3, 58.9, 4, -99.99, 5, 6.9, 6, 48.2, 7, 89,1, 8, 85.3, 9, 98.1, 10, 190.2, 11, 283.9, 12, -99.99

The output file can be called the year_mly.txt

If there are missing values for a specific month, it should be given a value of -99.99. This way, every ID has a value for each month of the specified year.

7
  • 3
    HI Kaitlin! This is a tiny bit too broad, because we're not a free code-writing service. We love to help, though! Can you explain what you've tried and where you're stuck? To me, this looks like if you want it in Python, you are probably somewhat familiar in Python and could easily read in the lines from a file, split each line across the space characters, and then just write the elements you need into the right file. Have you tried something similar? Commented Oct 22, 2024 at 15:58
  • (1) Rather than showing 34 lines of input data that are (AFAICT) identical, features-wise, it would be better to show a sufficient representative sample of input data and show what you want to get as output.   (1a) In particular, what do you want the 2024 file to look like (given that you, presumably, have data for some, but not all, months of the current year)?  In general, what do you want to happen if some month(s) are missing?   (2) What do you mean by “each initial file”?   (3) Do any of your data values include space(s)?  … (Cont’d) Commented Oct 22, 2024 at 17:04
  • (Cont’d) … (4) Are you saying that you have 3000+ files of approximately 537 lines each (one per month, January 1980 through the present), and you want to end up with 45 files (one per year) of 3000+ lines each?  Please clarify. Commented Oct 22, 2024 at 17:04
  • Yes, I have 3000+ files that I need to pare down to around 45 files (one per year of data). Each file would include the following columns in each line. IDENTIFICATION, YEAR, JANUARY MONTH CODE (1), JANUARY PRECIPITATION, FEBRUARY MONTH CODE (2), FEBRUARY PRECIPITATION, MARCH MONTH CODE (3), MARCH PRECIPITATION...... Commented Oct 22, 2024 at 17:19
  • Please edit your question and include everything people asked for in the comments. We need to see representative input, and the output you want from that example input. We also need to see the file names so we don't waste your time or ours giving solutions that don't work. Commented Oct 22, 2024 at 17:45

2 Answers 2

2

I created fake intput data like this:

#!/usr/bin/perl
use warnings;
use strict;
for my $id ('aaa' .. 'eod') {
    open my $out, '>', "file.$id" or die "file.$id: $!";
    for my $year (1979 .. 2023) {
        for my $month (1 .. 12) {
            printf {$out} "%d %d 0 %f\n", $year, $month, rand() / (1 + rand);
        }
    }
}

And then processed them by the following Perl script:

#!/usr/bin/perl
use warnings;
use strict;
use feature 'say';

my %by_year;

for my $file (glob 'file.???') {
    my $id = substr $file, -3;
    open my $in, '<', $file or die "$file: $!";
    while (my $line = <$in>) {
        my ($year, $month, undef, $precipitation) = split ' ', $line;
        die "Duplicate $id $year $month" if exists $by_year{$year}{$id}{$month};
        $by_year{$year}{$id}{$month} = $precipitation;
    }
}

for my $year (keys %by_year) {
    open my $out, '>', "$year.out" or die "$year.out: $!";
    for my $id (sort keys %{ $by_year{$year} }) {
        say {$out} join ' ', $id, map { $_, $by_year{$year}{$id}{$_} } 1 .. 12;
    }
}

You didn't show how exactly your files are named, so you'll have to adjust the line that does the glob as well as the one that extracts the id (using substr here).

3
  • The input files are all named like 101C0ME_A.txt with varying letter and number combinations. Commented Oct 22, 2024 at 17:30
  • I edited the original question to help answer some questions asked. Thank you for your help!!! Commented Oct 22, 2024 at 17:40
  • You can try tweaking my solution to your real data. Ask a new question if you get stuck. Commented Oct 22, 2024 at 19:38
0

Ahoy!

I was able to sort through it. The steps I followed were as follows.

  1. A separate script to create 3000 files of the format "YEAR, MONTH, REMOVE THIRD COLUMN, PRECIPITATION"

  2. The files created have a random name, an underscore, the order they were created, and a .txt extension i.e. 17920_2624.txt would be file number 2624

  3. The precipitation in the randomly created files is a random number between 0-250, and a decimal between 0-999 i.e. 229.370

  4. The main script will parse each of these 3000 files, and store the data in hash %filenameYears. The hash key is each filename, the hash value is a pointer to %allYearsInFile. %allYearsInFile will become an anonymous hash containing precipitation data for each year in the file

  5. The anonymous hash key is each year, the anonymous hash value is the output string of data for that year in the condensed formatted you requested

  6. If the precipitation data for any month is missing, substitute the missing value with -99.99

The scipt to create the output files looks like this...

#!/usr/bin/perl -w

my $minimum = 1980;
my $maximum = 2025;
my $nfiles = shift or die("no command line arg");
my $count = 0;
my @header = ("YEAR,", "MONTH,", "REMOVE THIRD COLUMN,", "PRECIPITATION");
my @lengthHeaders;           #for printf table formatting
for(@header){
  $lengthHeaders[$count] = "%-" . length($_) . "s";
  $count++;
}

for $count (1 .. $nfiles) {
  my $filename = int(rand(99999));
  $filename .="_${count}.txt";
  open my $out, '>', "$filename" or die "$filename: $!";
  printf($out "@lengthHeaders\n",@header);
  for my $year ($minimum .. $maximum) {
        for my $month (1 .. 12) {
      printf($out "@lengthHeaders\n", $year, $month, ,0, int(rand(250)) . "." .  int(rand(999)));
        }
    }
}

#run this command to print the files in the order they were created
#perl -e 'print "$_\n" for(sort { ($a =~ /_(\d+)\.txt/)[0] <=> ($b =~ /_(\d+)\.txt/)[0] } @ARGV)' *.txt

This script will generate however many files you specify. To create 3000 files, run the following command...

$ perl create.files.pl 3000

Here is the code to parse the input files and put them in the condensed format you described...

#!/usr/bin/perl -w

my @headers = ("IDENTIFICATION,", "YEAR,", "MONTH CODE,", "PRECIPITATION FOR THAT MONTH");
my @lengthHeaders;                                   #find length of each header for printf table formatting
my ($file,$filecount,$count) = ("",0,0);
for(@headers){
  $lengthHeaders[$count] = "%-" . length($_) . "s";
  $count++;
}
my %filenameYears;                                    #key is filename, value is a pointer to an anonymous hash containing data from all years in the file
my %allYearsInFile;                                    #key is year, value is output string of data for that year

while(<>){
  if($file ne $ARGV){                                   #filename being processed has just changed
    $filecount++;
    $file = $ARGV;                                       #update new filename
    next;                                                 #skip header lines
  }
  my %line;
  @line{("year","month","remove","precipitation")} = split(/ +/);
  if(!defined($allYearsInFile{$line{year}})){              #start a new year value
    #$allYearsInFile{$line{year}} = $line{year};
    $allYearsInFile{$line{year}} = sprintf("$lengthHeaders[1]", $line{year} . ",");
  }
  if( $line{precipitation} !~ /\d/){                        #if precipitation value is missing, set value to -99.99
    #DEBUG: warn "missing value $_";
    $line{precipitation} = -99.99;
  }
  #$allYearsInFile{$line{year}} .= ", $line{month}, $line{precipitation}";
  $allYearsInFile{$line{year}} .= sprintf( " %-4s%-8s", $line{month} . ",", $line{precipitation} . ",");
  if($line{month} == 12){
    $allYearsInFile{$line{year}} =~ s/, *$//;                #remove trailing comma
  }
  if(eof){                                                    #when file ends, save hash and start a new one for the next file
    $filenameYears{$file} = {%allYearsInFile};                 #save old hash as anonymous hash by filename
    %allYearsInFile = ();                                       #empty old hash for next year
  }
}

printf("@lengthHeaders\n",@headers);                             #done processing files, print headers

#DEBUG: print "Processed $filecount files\n";
for my $filename (sort keys %filenameYears){                      #dereference hash and print output string in formatted printf table
  my $hashref = $filenameYears{$filename};
  for my $year ( sort keys %$hashref ){
    #print "$filename, $$hashref{$year}\n";
    printf("@lengthHeaders[0..1]\n", $filename . ",", $$hashref{$year});
  }
}

To parse the 3000 files and put them in the condensed format you described, run the following command...

$ perl parse.precipitation.files.pl *.txt 

IDENTIFICATION, YEAR, MONTH CODE, PRECIPITATION FOR THAT MONTH
84406_1.txt,    1980, 1,  187.288, 2,  22.298,  3,  175.23,  4,  41.606,  5,  104.842, 6,  176.260, 7,  207.896, 8,  143.67,  9,  57.9,    10, 69.99,   11, 146.85,  12, 49.121
84406_1.txt,    1981, 1,  128.77,  2,  242.826, 3,  49.836,  4,  115.318, 5,  79.676,  6,  2.585,   7,  109.714, 8,  100.613, 9,  123.566, 10, 218.599, 11, 115.717, 12, 76.219
84406_1.txt,    1982, 1,  227.123, 2,  155.287, 3,  95.521,  4,  17.647,  5,  176.328, 6,  95.766,  7,  106.289, 8,  90.45,   9,  93.676,  10, 142.85,  11, 141.379, 12, 109.357
<cut>

This will parse all 3000 files sequentially and print the output in the format you requested. If you want to save this output in its own file, run the following command...

$ perl parse.precipitation.files.pl *.txt >  year_mly.txt

For all 3000 files it should take around 5 seconds to finish.

$ time perl parse.precipitation.files.pl *.txt > year_mly.txt

real    0m4.345s
user    0m4.282s
sys     0m0.061s

To separate this large file into files by year, i.e. one file containing all readings from all files for the year 1980, you can run the following script...

#!/usr/bin/perl -w

my $headerLine = 1;
my @headers = ("IDENTIFICATION", "YEAR", "MONTH CODE", "PRECIPITATION FOR THAT MONTH");
my @lengthHeaders;                                   #find length of each header for printf table formatting
my ($file,$filecount,$count) = ("",0,0);
for(@headers){
  $lengthHeaders[$count] = "%-" . length($_) . "s";
  $count++;
}

my %allFilesByYear;
while(<>){
  if($headerLine){
    $headerLine = 0;
    next;
  }
  my %line;
  @line{@headers} = split(/, +/);
  push(@{$allFilesByYear{$line{YEAR}}},$_);           #store each line in a hash where the key is each year, and the value is the row of data.
}

for $k (sort keys(%allFilesByYear)){                   #dereference hash
  my $arrayref = $allFilesByYear{$k};
  open my $out,'>',"${k}_mly.txt" or die "$!";          #create new file for each year
  for( @{$allFilesByYear{$k}} ){
    print $out "$_";                                     #output data for each year
  }
}

Run this script with the following command...

$ perl files.by.year.pl year_mly.txt

This will automatically separate all the data into smaller files based on the year. So all the readings from 1980 will be in the file 1980_mly.txt and so on for each year. You can also accomplish this manually using something like grep. To see all data for the year 1980 manually, you could run something like this...

more year_mly.txt | grep -i ' 1980,'

And to put this data in a file you could run something like this...

more year_mly.txt | grep -i ' 1980,' > data_from_1980.txt

That should be exactly what you are looking for. If the requirements arent all met just let me know what you need with some sample data.

Good Luck!

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.