packages feed

hanalyze-0.1.0.0: demo/io/PreprocessDemo.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
-- | Hanalyze.DataIO.Preprocess の総合デモ。
--
-- - NA 文字列を含む CSV をロード (Hackage dataframe 経由)
-- - countMissing で欠損列を確認
-- - dropMissingRows / imputeMean / imputeMedian / imputeConstant の比較
-- - filterRowsByNumeric / mapNumeric / deriveNumeric の使用例
module Main where

import qualified Data.Map.Strict as Map
import qualified Data.Text as T

import qualified DataFrame                    as DX
import qualified DataFrame.Internal.DataFrame as DXD
import Hanalyze.DataIO.CSV         (loadCSV)
import Hanalyze.DataIO.Preprocess

import System.IO          (hPutStrLn, stderr)
import System.Exit        (exitFailure)
import Text.Printf        (printf)

testCSV :: String
testCSV = unlines
  [ "group,age,income"
  , "A,25,40000"
  , "A,NA,42000"
  , "B,32,"
  , "B,28,55000"
  , "C,,38000"
  , "A,45,NA"
  , "B,30,48000"
  , "C,55,72000"
  ]

main :: IO ()
main = do
  let path = "/tmp/preprocess_demo.csv"
  writeFile path testCSV

  result <- loadCSV path
  case result of
    Left err -> do
      hPutStrLn stderr ("Parse error: " ++ err)
      exitFailure
    Right df -> runDemo df

runDemo :: DXD.DataFrame -> IO ()
runDemo df = do
  putStrLn "=================================="
  putStrLn " Hanalyze.DataIO.Preprocess Demo"
  putStrLn "=================================="
  putStrLn ""
  let (nrows, _) = DX.dimensions df
  printf "Loaded %d rows, columns: %s\n"
         nrows (T.unpack (T.intercalate ", " (DX.columnNames df)))
  putStrLn ""

  putStrLn "--- countMissing ---"
  mapM_ (\(c, m) ->
    if m > 0 then printf "  %s: %d missing\n" (T.unpack c) m
             else printf "  %s: complete\n"   (T.unpack c))
    (countMissing df)
  putStrLn ""

  putStrLn "--- dropMissingRows [\"age\", \"income\"] ---"
  let df1 = dropMissingRows ["age", "income"] df
      (nrows1, _) = DX.dimensions df1
  printf "  After: %d rows (was %d)\n" nrows1 nrows
  putStrLn ""

  putStrLn "--- parseNumericColumn ---"
  case parseNumericColumn "age" df1 >>= parseNumericColumn "income" of
    Nothing -> putStrLn "  (already numeric or parse failed; OK if Hackage parsed it)"
    Just df2 -> do
      printf "  Both age/income are now numeric\n"
      showNumericStats df2 "age"
      showNumericStats df2 "income"
  putStrLn ""

  putStrLn "--- imputeMean / imputeMedian on age ---"
  case imputeMean "age" df of
    Just df3 -> do
      let (n3, _) = DX.dimensions df3
      printf "  imputeMean produces %d numeric rows\n" n3
      showNumericStats df3 "age"
    Nothing -> putStrLn "  imputeMean failed"
  case imputeMedian "income" df of
    Just df4 -> do
      let (n4, _) = DX.dimensions df4
      printf "  imputeMedian produces %d numeric rows\n" n4
      showNumericStats df4 "income"
    Nothing -> putStrLn "  imputeMedian failed"
  putStrLn ""

  putStrLn "--- filterRowsByNumeric (age >= 30) ---"
  let dfNum = case imputeMean "age" df >>= imputeMean "income" of
                Just d  -> d
                Nothing -> df
      dfFilt = filterRowsByNumeric "age" (>= 30) dfNum
      (nNum, _)  = DX.dimensions dfNum
      (nFilt, _) = DX.dimensions dfFilt
  printf "  After: %d rows (was %d)\n" nFilt nNum
  putStrLn ""

  putStrLn "--- mapNumeric \"income\" (/1000) ---"
  let dfMap = mapNumeric "income" (/ 1000) dfNum
  showNumericStats dfMap "income"
  putStrLn ""

  putStrLn "--- deriveNumeric \"ratio\" = income / age ---"
  let dfDeriv = deriveNumeric "ratio"
                  (\row -> case (Map.lookup "income" row, Map.lookup "age" row) of
                             (Just (VNum i), Just (VNum a)) | a > 0 -> i / a
                             _ -> 0)
                  dfNum
  showNumericStats dfDeriv "ratio"
  putStrLn ""

  putStrLn "--- selectColumns [\"group\", \"age\"] ---"
  let dfSel = selectColumns ["group", "age"] dfNum
  printf "  columns: %s\n" (T.unpack (T.intercalate ", " (DX.columnNames dfSel)))
  putStrLn ""

  putStrLn "Done."

showNumericStats :: DXD.DataFrame -> T.Text -> IO ()
showNumericStats df name =
  case readNum name df of
    Nothing -> printf "  %s: not numeric\n" (T.unpack name)
    Just xs -> do
      let m  = length xs
          mean = sum xs / fromIntegral m
          mn = minimum xs
          mx = maximum xs
      printf "  %-10s n=%d  min=%.2f  max=%.2f  mean=%.2f\n"
             (T.unpack name) m mn mx mean

readNum :: T.Text -> DXD.DataFrame -> Maybe [Double]
readNum name df =
  case DXD.getColumn name df of
    Nothing -> Nothing
    Just _  ->
      case tryReadDouble name df of
        Just xs -> Just xs
        Nothing -> tryReadIntAsDouble name df

tryReadDouble :: T.Text -> DXD.DataFrame -> Maybe [Double]
tryReadDouble name df = either (const Nothing) Just $
  fmap (map (id :: Double -> Double)) $
    Right (DX.columnAsList (DX.col @Double name) df)

tryReadIntAsDouble :: T.Text -> DXD.DataFrame -> Maybe [Double]
tryReadIntAsDouble name df = either (const Nothing) Just $
  fmap (map (fromIntegral :: Int -> Double)) $
    Right (DX.columnAsList (DX.col @Int name) df)