Bài toán phân tích dữ liệu ngày tháng năm sinh để báo cáo sơ bộ các kiểu dữ liệu hiện có

Phiên bản nâng cấp của bài viết này đã có ở đây: Hàm PHP thông báo đặc điểm dữ liệu ngày tháng sinh đầu vào với lượng dữ liệu lớn (v1.2).

Trong bài viết trước tôi có lảm nhảm về chuyện viết mã quá cẩn thận dẫn đến việc xử lý chậm dữ liệu ngày tháng. Tuy nhiên khi nhìn lại đống dữ liệu hiện có thì quả là việc sử dụng mã tổng quát rất quan trọng. Ít nhất cũng trong giai đoạn phân tích dữ liệu ban đầu để đề ra phương hướng giải quyết chuẩn xác. Vì khi dữ liệu càng lớn, việc có nhiều định dạng dữ liệu khác nhau càng nhiều. Và việc cứ thấp thỏm dữ liệu sai thật là khó chịu!

Bài toán mà tôi muốn giải quyết ngày hôm nay là, với một dữ liệu ngày tháng năm bất kỳ cho trước, cần phân tích được dữ liệu đó có bao nhiêu hàng là:

  • Dữ liệu trống;
  • Thiếu dữ liệu không thể khôi phục (chỉ có nhiều nhất là 2 trong ba dữ liệu, trường hợp thiếu này không thể khôi phục được);
  • Dữ liệu sai logic, ví dụ năm sinh 1700 cho người sống, và năm sinh 2100 cho tương lai còn chưa tới;
  • Trong tập dữ liệu có những kiểu dữ liệu nào, số lượng bao nhiêu: Ví dụ bao nhiêu dữ liệu là năm đứng trước (không điển hình với kiểu dữ liệu ngày tháng năm của VN), bao nhiêu là tháng đứng trước (không điển hình với kiểu dữ liệu ngày tháng năm của VN), bao nhiêu dữ liệu tuân thủ cấu trúc tiêu chuẩn ở VN (theo thứ tự ngày, tháng rồi năm);

Cái này nên viết thành hàm function, với biến đầu vào là tên bảng dữ liệu có chứa cột ngày tháng năm sinh và tên của cột ngày tháng, ví dụ ($ten_bang,$ngay). Việc này sẽ giúp chúng ta nhanh chóng kiểm tra một bảng dữ liệu bất kỳ thay vì liên tục phải sửa mã.

Đoạn mã ban đầu thực hiện với lấy tên bảng và kết nối csdl:

function kiem_tra_ngaythang($ten_bang,$ngay){ //tên bảng và tên cột là biến đầu vào
require 'database.php'; // kết nối csdl
$query3="SELECT ngay FROM $ten_bang";
$result3=$db->query($query3);

foreach ($result3 as $value3) {
        $ntn_goc=$value3['$ngay']; // đây là thông tin ngày tháng năm gốc lấy từ bảng
}

Tiếp theo chúng ta sẽ tiến hành cắt bỏ khoảng trắng trái và phải của dữ liệu :

$ngaya= trim($ntn_goc, ' '); // loại bỏ khoảng trắng trước và sau chuỗi ngày tháng năm sinh

Tạo biến đếm dữ liệu rỗng hoặc NULL, rồi dùng hàm if để kiểm tra:

$kdl=0; //lưu ý các biến như dạng $kdl cần đặt ngoài vòng lặp foreach
//bây giờ bạn cứ quan sát để hiểu, mã tổng thể có ở cuối bài
if ($ngaya==NULL || $ngaya == "") {$kdl++;}

Dù hiếm gặp, thi thoảng dữ liệu ngày tháng năm sinh cũng có khoảng trắng bên trong chuỗi, ví dụ 5/ 7/2015. Chúng ta cũng nên kiểm tra xem số lượng dữ liệu như vậy có nhiều hay không. PHP có câu lệnh để làm việc này rất nhanh gọn, nó có tên là substr_count($text, ' ').

Vì chúng ta đã khử khoảng trắng ở 2 bên, nên nếu giá trị khoảng trắng của chuỗi lớn hơn 0, thì có nghĩa là có khoảng trắng nằm trong chuỗi. Chúng ta cũng nên tạo một mảng để lưu các giá trị đó để in ra, tìm thủ công dữ liệu lỗi trong dữ liệu lớn rất khó khăn, nếu chúng ta có thông tin dữ liệu lỗi như thế nào thì việc tìm nó trong bảng dữ liệu rất dễ dàng.

$ktrang=array(); $j=0;
$cokt=substr_count($ngaya, ' ');

if ($cokt) {$ktrang[$j]=$ngaya; $j++;}
// thay vì viết $cokt > 0 chúng ta có thể viết ngắn gọn như trên
// thay vì viết $j=$j+1; chúng ta viết $j++ cũng cho kết quả tương tự

Tiếp theo chúng ta sẽ xem có bất kỳ ký tự nào quái lạ trong ngày tháng hay không hoặc nó bị thiếu dữ liệu không thể khôi phục không (chỉ có 2 trong 3 dữ liệu). Ký tự tiêu chuẩn trong ngày tháng chỉ bao gồm:

  • Dấu gạch xuôi /
  • Dấu gạch ngang -
  • Dấu chấm . (ít gặp hơn đáng kể so với hai dấu trên);
  • Và các con số chắc chắn phải có (0,1,2,..,9);

Chúng ta sẽ sử dụng RegEx để làm mẫu dữ liệu, nó sẽ như thế này:

^(\d{1,4})([\/\-\.]\d{1,4}){2}$

Đây là mẫu RegEx đơn giản, vì chúng ta chỉ muốn lọc dữ liệu có ký tự lạ (không bao gồm số và dấu phân cách / hoặc – hoặc .)

$laloi=array();$ll=0;

$reg_laloi='/^(\d{1,4})([\/\-\.]\d{1,4}){2}$/';

// bạn chú ý cách đặt lệnh regex trong PHP, nó thường nằm trong dấu '//'

$kq_laloi=preg_match($reg_laloi, $ngaya);

// nếu khớp kết quả trả về 1, nếu không khớp kết quả trả về 0

if ($kq_laloi==0) {$laloi[$ll]=$ngaya;$ll++;} // nếu ngày tháng năm có ký tự lạ hoặc thiếu dữ liệu thì nó được đưa vào mảng $laloi để show ra sau này

Vậy là đến bước này chúng ta đã làm được mấy việc quan trọng:

  • Phát hiện dữ liệu ngày tháng năm có khoảng trắng nằm trong;
  • Phát hiện dữ liệu ngày tháng năm có ký tự không được phép;
  • Phát hiện dữ liệu ngày tháng năm thiếu dữ liệu không thể khôi phục được;

Bây giờ mới đến bước chúng ta xác định dữ liệu có cấu trúc như thế nào.

Trong phần này, chúng ta cũng sẽ dùng biểu thức chính quy (RegEx), nhưng với cấu trúc chặt chẽ hơn nhiều.

Biểu thức chính quy để xác định cấu trúc ngày rồi đến tháng, rồi mới đến năm là:

^(0?[1-9]|[12]\d|3[01])[\/\-\.](0?[1-9]|1[012])[\/\-\.](\d{2}|\d{4})$
  • Chuỗi số đầu tiên sẽ chỉ nằm trong khoảng từ 01 đến 31 (hoặc 1, 2, 3,.., 9, không bắt buộc phải có số 0 đằng trước với những số dưới 10), chuỗi thứ hai chỉ nằm trong trong từ 01 (hoặc 1, không bắt buộc phải có số 0 đằng trước với những số dưới 10) đến 12. Còn cuối cùng là năm với giá trị là 2 số (ví dụ 87 hoặc 15) hay 4 số (ví 1987 hoặc 2015);
  • Đây là cấu trúc nhiều khả năng nhất sẽ xảy ra trong hầu hết trường hợp;

Câu lệnh cụ thể sẽ như sau:

$dem_ntnc=0; // dùng để đếm xem có bao nhiêu dữ liệu đạt chuẩn
$reg_ntnc='/^(0?[1-9]|[12]\d|3[01])[\/\-\.](0?[1-9]|1[012])[\/\-\.](\d{2}|\d{4})$/';
$kq_ntnc=preg_match($reg_ntnc, $ngaya);
if ($kq_ntnc) {$dem_ntnc++;}

Giá trị $dem_ntnc sẽ cho bạn biết trong dữ liệu của bạn có bao nhiêu dữ liệu đúng chuẩn ngày/tháng/năm (dấu / có thể là – hoặc .)

Chia con số đấy cho hàng dữ liệu, bạn có thể ước tính được tỷ lệ thống nhất dữ liệu của bạn. Nếu dữ liệu thu thập có nguồn đủ tốt, con số này thường lớn hơn 99%.

Đoạn mã hoàn chỉnh để kiểm tra:

<?php
function kiem_tra_ngaythang_v2($ten_bang,$ngay){ // để kiểm tra dữ liệu ngày tháng đang có cấu trúc như thế nào?
require 'database.php';
$query3="SELECT ngay FROM $ten_bang";
$result3=$db->query($query3);

$kdl=0;//đếm dữ liệu NULL
$ktrang=array(); $j=0; // đếm và lưu dữ liệu có khoảng trắng ở giữa
$laloi=array();$ll=0; // đếm và lưu dữ liệu lỗi
$dem_ntnc=0; // Đếm số dữ liệu đạt tiêu chuẩn ngày/tháng/năm
$dem_tnnc=0; // Đếm số dữ liệu đạt tiêu chuẩn tháng/ngày/năm
$tong_ban_ghi=0;//Tổng số hàng dữ liệu
  foreach ($result3 as $value3) {
        $tong_ban_ghi++;
        $ntn_goc=$value3[$ngay]; // đây là thông tin ngày tháng năm gốc lấy từ bảng
        $ngaya= trim($ntn_goc, ' '); // loại bỏ khoảng trắng trước và sau chuỗi ngày tháng năm sinh
        
        if ($ngaya==NULL || $ngaya == "") {$kdl++;} else {
                
               $cokt=substr_count($ngaya, ' ');
               if ($cokt) {$ktrang[$j]=$ngaya; $j++;} else {
        
                       $reg_laloi='/^(\d{1,4})([\/\-\.]\d{1,4}){2}$/';
                       $kq_laloi=preg_match($reg_laloi, $ngaya);
                       
                       // nếu ngày tháng năm có ký tự lạ hoặc thiếu dữ liệu thì nó được đưa vào mảng $laloi để show ra sau này
                       if ($kq_laloi==0) {$laloi[$ll]=$ngaya;$ll++;}
                        
                }
                 
        }
          // kiểm tra khớp với kiểu dữ liệu ngày tháng năm
          $reg_ntnc='/^(0?[1-9]|[12]\d|3[01])[\/\-\.](0?[1-9]|1[012])[\/\-\.](\d{2}|\d{4})$/';
          $kq_ntnc=preg_match($reg_ntnc, $ngaya);
          if ($kq_ntnc) {$dem_ntnc++;}
          
          // kiểm tra khớp với kiểu dữ liệu tháng ngày năm
          $reg_tnnc='/^(0?[1-9]|1[012])[\/\-\.](0?[1-9]|[12]\d|3[01])[\/\-\.](\d{2}|\d{4})$/';
          $kq_tnnc=preg_match($reg_tnnc, $ngaya);
          if ($kq_tnnc) {$dem_tnnc++;}
    } 

$kq_ktrang="";  
$sl_ktrang=count($ktrang);
if($sl_ktrang>0) {foreach ($ktrang as $ch_ktrang) {$kq_ktrang=$kq_ktrang.$ch_ktrang."</br>";}}

$in_laloi="";
$sl_laloi=count($laloi);
if($sl_laloi>0) {foreach ($laloi as $ch_laloi) {$in_laloi=$in_laloi.$ch_laloi."</br>";}}

$tyle=($dem_ntnc/$tong_ban_ghi)*100;
$kq_tyle=$tyle.'%';
$tong_dll=$tong_ban_ghi-$dem_ntnc; // tổng dữ liệu lỗi

$giadinh=($dem_tnnc/$tong_ban_ghi)*100;
$kq_giadinh=$giadinh.'%';

$kq1="Số lượng dữ liệu NULL hoặc RỖNG: ".$kdl."</br>";
$kq2="Số lượng dữ liệu có khoảng trắng trong ngày tháng (hiếm gặp): ".$sl_ktrang."</br>"."Các mẫu (nếu có): "."</br>".$kq_ktrang."</br>"."</br>";
$kq3="Số lượng dữ liệu có ký tự lạ hoặc thiếu dữ liệu không khôi phục được: ".$sl_laloi."</br>"."Các mẫu (nếu có): "."</br>".$in_laloi."</br>"."</br>";
$kq4="Tỷ lệ dữ liệu có cấu trúc ngày/tháng/năm tiêu chuẩn: ".$kq_tyle."</br>";
$kq5="Tổng số dữ liệu khác cấu trúc tiêu chuẩn là: ".$tong_dll."</br>";
$kq6="Giả định dữ liệu của bạn là tháng/ngày/năm thì tỉ lệ sẽ là: ".$kq_giadinh."</br>";
// ngày tháng năm tiêu chuẩn là có cấu trúc ngày trước tháng rồi cuối là năm, dấu phân cách có thể là
// dấu gạch xuôi / hoặc dấu gạch ngang - hoặc dấu .
// ngày có thể là 1 số hoặc 2 số, có thể có số 0 đằng trước nếu dưới 10, ngày nằm trong khoảng từ 1 đến 31
// tháng có thể là 1 số hoặc 2 số, có thể có số 0 đằng trước nếu dưới 10, tháng chỉ nằm trong khoảng từ 1 đến 12
// năm có thể là số có 2 chữ số hoặc 4 chữ số
// dù dữ liệu của bạn là ngày/tháng/năm chuẩn thì những ngày như 05/07/2015 thì giả định tháng đứng trước vẫn hợp lệ theo cách kiểm tra
// do vậy thực tế dữ liệu rất quan trọng, thường những dữ liệu lấy từ cùng một nguồn sẽ có cấu trúc giống nhau
$kq=$kq1.$kq2.$kq3.$kq4.$kq5.$kq6;
return $kq;
}
echo kiem_tra_ngaythang_v2("ten_bang_du_lieu","ten_cot_ngay");
// bạn thay thế ten_bang_du_lieu bàng tên bảng dữ liệu của bạn (không phải tên database)
// bạn thay thế ten_cot_ngay bằng tên của cột ngày tháng trong bảng dữ liệu mà bạn cần kiểm tra

Ví dụ về một kết quả mà tôi kiểm tra với gần 40 ngàn hàng dữ liệu:

Nếu dữ liệu đầu vào tốt, hầu như bạn chỉ gặp lỗi dữ liệu NULL, rỗng chứ ít khi gặp các vấn đề về ký tự lạ hoặc có cách trắng ở trong tên.

PS: ngoài ra bạn có thể dùng hàm sau để xuất đầy đủ dữ liệu không hợp chuẩn ngày tháng năm nhưng không phải là RỖNG hoặc NULL.

<?php
function ngay_thang_ktc($ten_bang,$ngay){ // để kiểm tra dữ liệu không phải tiêu chuẩn ngày/tháng/năm
require 'database.php';
$query4="SELECT ngay FROM $ten_bang";
$result4=$db->query($query4);

$none_dem_ntnc=array();$j=0; // Đếm số dữ liệu không phải chuẩn ngày tháng năm
  foreach ($result4 as $value4) {
        $ntn_goc=$value4[$ngay]; // đây là thông tin ngày tháng năm gốc lấy từ bảng
        $ngaya= trim($ntn_goc, ' '); // loại bỏ khoảng trắng trước và sau chuỗi ngày tháng năm sinh
        
        if ($ngaya!=NULL || $ngaya != "") {
          // kiểm tra khớp với kiểu dữ liệu ngày tháng năm
          $reg_ntnc='/^(0?[1-9]|[12]\d|3[01])[\/\-\.](0?[1-9]|1[012])[\/\-\.](\d{2}|\d{4})$/';
          $kq_ntnc=preg_match($reg_ntnc, $ngaya);
          if ($kq_ntnc==0) {$none_dem_ntnc[$j]=$ngaya;$j++;} //bằng 0 nghĩa là không khớp
        }     
  } 

$kq_none_ntn="";  
$sl_none_ntn=count($none_dem_ntnc);
if($sl_none_ntn>0) {foreach ($none_dem_ntnc as $ch_none_dem_ntnc) {$kq_none_ntn=$kq_none_ntn.$ch_none_dem_ntnc."</br>";}}

$kq="Các dữ liệu hệ thống ghi nhận khác tiêu chuẩn ngày/tháng/năm nhưng không phải là RỖNG hoặc NULL: "."</br>".$kq_none_ntn."</br>";
return $kq;
}

Ví dụ từ một bảng của tôi:

Xuất dữ liệu không phải chuẩn ngày/tháng/năm

Xin chào và hẹn gặp lại các bạn trong bài viết sau khi tôi sẽ xử lý tổng quát hơn trường giới tính.

Leave a Comment