dump more info about duplicate input rows
[csv-join-similarity] / upari.pl
1 #!/usr/bin/perl
2
3 use warnings;
4 use strict;
5 use autodie;
6
7 # apt install libtext-csv-perl libstring-similarity-perl
8 use Text::CSV;
9 use Data::Dump qw(dump);
10 use String::Similarity;
11 use Storable;
12
13 my $debug = $ENV{DEBUG};
14
15 my @files = qw( 1.csv 2.csv 3.csv 4.csv );
16
17 our $stat;
18 my $keys;
19
20 sub duplicate {
21         my @for = @_;
22         my $use;
23         $use->{$_}++ foreach ( map { @{ $stat->{_}->{$_} } } @for );
24         my $duplicate = grep { $use->{$_} > 1 } keys %$use;
25         print "XXX use @for ",dump($use),$/ if $debug && $duplicate;
26         return $duplicate;
27 }
28
29 sub candidates {
30         my ( $num, $key_id, $limit_sim ) = @_;
31         $limit_sim //= $ENV{LIMIT};
32         $limit_sim //= 0.9;
33
34         my @candidates;
35         foreach my $key ( sort keys %{ $keys->{ $key_id } } ) {
36                 next if $key eq $num; # XXX don't return key from input
37                 my $s = similarity $num, $key, $limit_sim;
38                 #warn "# $num $key $s\n";
39                 if ($s > $limit_sim ) {
40                         if ( exists $stat->{_}->{"$key-$key_id"} ) {
41                                 my $use;
42                                 $use->{$_}++ foreach (
43                                         @{ $stat->{_}->{"$num-$key_id"} },
44                                         @{ $stat->{_}->{"$key-$key_id"} },
45                                 );
46                                 #print "XXX use $num $key ",dump($use);
47                                 my $duplicate = grep { $use->{$_} > 1 } keys %$use;
48                                 if ( $duplicate ) {
49                                         print "XXX $limit_sim suggest duplicate  $num $key SKIP duplicate ",dump($use), $/ if $debug;
50                                         $stat->{suggest}->{duplicate}++;
51                                 } else {
52                                         push @candidates, { key => $key, s => $s };
53                                 }
54                         } else {
55                                 print "XXX $limit_sim candidates $key missing\n" if $debug;
56                         }
57                 }
58         }
59 =for description
60         for limit 0.6 to work, we need to prefer longer results over shorter ones:
61         MERGE A0246A 3078-8326 (
62           { key => "A4065A", s => 0.666666666666667 },
63           { key => "ANDREJA0246A", s => 0.666666666666667 },
64   ) val=[3]
65   .++ A0246A-3078-8326 1 .++ A0246A-3078-8326 2 result val=[3, 1, 2] result_elements=3
66 =cut
67
68         if ( $#candidates > 0 ) {
69                 #print "XXX candidates before = ",dump( \@candidates ),$/;
70                 @candidates = sort {
71                         $b->{s} <=> $a->{s}     # hi -> low
72                         or length($b->{key}) <=> length($a->{key})
73                 } @candidates;
74                 #print "XXX candidates after  = ",dump( \@candidates ),$/;
75         }
76         return @candidates;
77 }
78
79 my $keys_file = 'keys.storable';
80 if ( -e $keys_file ) {
81         #$keys = retrieve($keys_file) or die "$keys_file: $!";
82         print "LOAD $keys_file", scalar keys %$keys, "\n";
83 }
84
85 our $data;
86 our @data_headers;
87
88 foreach my $val ( 1 .. 4 ) {
89         my $file = "$val.csv";
90         warn "# $file\n";
91
92         my $csv = Text::CSV->new ({ binary => 1, auto_diag => 1 });
93         open my $fh, "<:encoding(utf8)", $file or die "$file: $!";
94         while (my $row = $csv->getline ($fh)) {
95                 $stat->{file}->{$file}->{lines}++;
96                 $stat->{file}->{$file}->{columns}->{ $#$row }++;
97
98                 if ( ! exists $data_headers[$val] ) {
99                         $data_headers[$val] = $row;
100                         next;
101                 }
102
103                 my $c_id = $row->[0];
104                 my $c_s = $row->[1];
105                 my $c_r = $row->[2];
106                 $row->[0] =~ s/[^\w\d]+//ig && $stat->{file}->{$file}->{corrupt_id}->{$c_id}++ && print 'c0';
107                 $row->[1] =~ s/\D+//g && $stat->{file}->{$file}->{corrupt_s}->{$c_s}++ && print 'c1';
108                 $row->[2] =~ s/\D+//g && $stat->{file}->{$file}->{corrupt_r}->{$c_r}++ && print 'c2';
109                 my $id = join('-',
110                         uc $row->[0],
111                         $row->[1],
112                         $row->[2],
113                 );
114
115                 my $num = uc $row->[0];
116                 if ( length $num < 3 ) {
117                         print "IGNORE $val ",dump($row->[ 0 .. 5 ]),$/;
118                         $stat->{ignore}->{$val}++;
119                         next;
120                 }
121
122                 my $key_id = $row->[1] . '-' . $row->[2];
123
124                 $stat->{A_key_id}->{$key_id}->{$val}++;
125
126                 $keys->{ $key_id }->{ $num }++;
127
128                 $stat->{exists}->{$val}++ if exists $stat->{_}->{ $id };
129
130                 push @{ $stat->{_}->{ $id } }, $val;
131
132                 if ( exists $data->{$key_id}->{$num}->{$val} ) {
133                         $stat->{file}->{$file}->{duplicate_keyid_num}->{$val}++;
134                         print "DUPLICATE $key_id $num $val old elements=", $#{ $data->{$key_id}->{$num}->{$val} }, "new ", $#$row, $/;
135                         my $diff;
136                         my $old = $data->{$key_id}->{$num}->{$val};
137                         foreach ( 0 .. $#$row ) {
138                                 if ( $old->[$_] ne $row->[$_] ) {
139                                         $diff->[$_] = [ $old->[$_], $row->[$_] ];
140                                 }
141                         }
142                         print "diff = ",dump($diff) if $diff;
143                         #print "old=", dump( $data->{$key_id}->{$num}->{$val} ), $/;
144                         #print "new=", dump( $row ), $/;
145                 }
146                 $data->{$key_id}->{$num}->{$val} = $row;
147         }
148         close $fh;
149
150 }
151
152
153 my $merge_ids;
154
155 my $first = 1;
156 # 0.9 - 0.7 -- 0.6 is too lax
157 foreach my $limit ( 0.7 ) { #, 0.6 ) {
158 warn "XXX limit $limit\n";
159
160 print "# total = ",scalar keys %{ $stat->{_} }, $/;
161 foreach my $id ( sort keys %{ $stat->{_} } ) {
162         my @val;
163         my $v = $stat->{_}->{$id};
164         if ( defined $v && ref $v eq 'ARRAY' ) {
165                 @val = @$v;
166         } else {
167                 #print "SKIP[$id]";
168                 next;
169         }
170         my $u;
171         $u->{$_}++ foreach @val;
172         my @u_v = sort keys %$u;
173         if ( $first == 1 ) {
174                 $stat->{A_count}->{ scalar @val }++;
175                 $stat->{A_count_total}++;
176
177                 #$stat->{A_count_val_dup}->{ join(' ', @val) }++; # with duplicates
178
179                 $stat->{A_count_val}->{ join(' ', @u_v ) }++; # without duplicates
180         }
181
182         if ( $#u_v < 3 ) { # single, double
183                 my ( $num, $key_id ) = split(/-/,$id,2);
184                 my @candidates = candidates $num => $key_id, $limit; #, 0.7; # XXX 0.9 too high, 0.8 better, 0.7 too lax
185                 if ( @candidates ) {
186                         print "MERGE ",scalar @candidates, " $limit $num $key_id ", dump( @candidates ), ' val=', dump( \@val ), $/;
187                         my @keys = map { $_->{key} } @candidates;
188                         my  $m_id = $id; # "$keys[0]-$key_id";
189                         foreach my $i ( 0 .. $#keys ) {
190                                 my  $id = "$keys[$i]-$key_id";
191                                 if ( ! exists $stat->{_}->{$id} ) {
192                                         print "ERROR: $num $key_id can't find $i $id";
193                                 }
194
195                                 # XXX I298O-4743-7996
196                                 if ( duplicate( $m_id => $id ) ) {
197                                         print "XXX duplicate2 $m_id $id\n";
198                                         $stat->{duplicate2}++;
199                                         next;
200
201                                 }
202
203                                 my $o = delete $stat->{_}->{$id};
204                                 die "FATAL: can't find $id" if ! $o;
205
206                                 my ( $id_s, $s, $r ) = split('-', $id);
207                                 my $key_s = "$s-$r";
208
209                                 foreach my $val ( @$o ) {
210                                         print '.';
211                                         push @{ $stat->{_}->{ $m_id } }, $val;
212                                         print "++ $m_id $val ";
213                                         $stat->{merge_val}->{$val}++;
214
215
216                                         die "ERROR merge: $val $id $m_id exists",dump( $merge_ids->{$val}->{$key_s}->{$id_s} ) if exists $merge_ids->{$val}->{$key_s}->{$id_s};
217                                         my $m_id_s = (split('-',$m_id,3))[0];
218                                         $merge_ids->{$val}->{$key_s}->{$id_s} = $m_id_s;
219
220                                         my $o_row = delete $data->{$key_s}->{$id_s}->{$val};
221                                         die "FATAL: $id | $m_id | data $key_s $id_s $val" unless $o_row;
222                                         $data->{$key_s}->{$m_id_s}->{$val} = $o_row;
223
224                                 }
225                                 my @not_empty = sort keys %{ $data->{$key_s}->{$id_s} };
226                                 die "FATAL: $id_s not empty" if @not_empty;
227                                 delete $data->{$key_s}->{$id_s}; # FIXME check before cleanup
228
229                                 print "result val=",dump( $stat->{_}->{ $m_id } ), " result_elements=", scalar @{ $stat->{_}->{ $m_id } }, $/;
230                         }
231                         $stat->{merge}++;
232                 }
233
234         }
235 }
236
237 print "# total after merge $limit = ",scalar keys %{ $stat->{_} }, $/;
238
239 foreach my $id ( sort keys %{ $stat->{_} } ) {
240         my @val; # = @{ $stat->{_}->{$id} };
241         my $v = $stat->{_}->{$id};
242         if ( defined $v && ref $v eq 'ARRAY' ) {
243                 @val = @$v;
244         } else {
245                 print "SKIP[$id]";
246                 next;
247         }
248         $stat->{"B${limit}_count"}->{ scalar @val }++;
249         $stat->{"B${limit}_count_total"}++;
250
251         #$stat->{"B${limit}_count_val_dup"}->{ join(' ', @val) }++; # with duplicates
252
253         my $u;
254         $u->{$_}++ foreach @val;
255         my @u_v = sort keys %$u;
256         $stat->{"B${limit}_count_val"}->{ join(' ', @u_v ) }++; # without duplicates
257 }
258
259 $first++;
260 } # for $limit
261
262 print "# stat = ",dump( $stat );
263 #print "# keys = ",dump( $keys );
264
265 store $keys, $keys_file;
266
267 my $merge_file = 'merge.storable';
268 store $merge_ids, $merge_file;
269 #print "XXX merge_ids = ", dump($merge_ids);
270
271 my $out_file = 'merged.csv';
272 print "out_file $out_file";
273 open(my $out_fh, '>', $out_file);
274 foreach my $val ( 1 .. 4 ) {
275         print $out_fh join(',', map { $_ . '_' . $val } @{ $data_headers[$val] });
276         print $out_fh ',' if $val < 4;
277 }
278 print $out_fh "\n";
279
280 foreach my $key ( sort keys %$data ) {
281         print " $key";
282         foreach my $id ( sort keys %{ $data->{$key} } ) {
283                 #print $out_fh "## $id ## ";
284                 foreach my $val ( 1 .. 4 ) {
285                         if ( my $id_data = $data->{$key}->{$id}->{$val} ) {
286                                 print $out_fh join(',', @$id_data);
287                         } else {
288                                 print $out_fh ( ',' x $#{ $data_headers[$val] } ) ; # FIXME +1?
289                         }
290                         print $out_fh ',' if $val < 4;
291                 }
292                 print $out_fh "\n";
293         }
294 }
295 close $out_fh;
296 print "\n", -s $out_file, " bytes created\n";
297