packages feed

hanalyze-0.2.0.0: src/Hanalyze/Data/Factor.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : Hanalyze.Data.Factor
-- Description : forcats 流の因子 (factor) 型と水準操作 (fct_* 相当)
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- forcats 流の因子 (factor) 型操作 (Phase 28 Ch16 "Factors")。
--
-- R の `factor` を **水準 (levels) の順序付きリスト + 各観測の水準コード**として
-- 表す 'Factor' 型と、 forcats の `fct_*` 相当を純粋関数として公開する
-- ('Data.Strings' / 'Data.Transform' と同列の `Data/` 純粋抽象)。
--
-- === なぜ専用型か
-- ただの @[Text]@ と違い factor は (1) 水準の**意味的順序** (アルファベット順とは別)、
-- (2) データに現れない水準の保持、 (3) 整数コード化、 を持つ。 forcats の `fct_*` は
-- この水準順序や中身を操作する関数群で、 順序概念のない @[Text]@ には無い。
--
-- === HBM の Column.Factor との違い
-- 'Hanalyze.Model.HBM' の内部 @Column = Numeric | Factor@ は NUTS に渡す
-- 観測列の内部表現。 本 'Factor' は **データ整形ドメイン**の公開型で責務が別 (独立実装)。
--
-- === コード規約
-- 'facCodes' は **0 始まり** (@facLevels !! code@ で復元)。 欠損 (R の @\<NA\>@) は
-- コード @-1@ で表す ('naCode')。
module Hanalyze.Data.Factor
  ( -- * 型
    Factor (..)
  , naCode
    -- * 生成 (factor / fct / ordered)
  , factor
  , factorWith
  , fct
  , ordered
    -- * 参照 (levels / as.character / count)
  , levels
  , isOrdered
  , asTexts
  , asTextsMaybe
  , fctCount
    -- * 順序操作 (16.4 forcats fct_reorder 系)
  , fctReorder
  , fctRelevel
  , fctReorder2
  , fctInfreq
  , fctRev
    -- * 水準操作 (16.5 forcats fct_recode / fct_collapse / fct_lump 系)
  , fctRecode
  , fctCollapse
  , fctLumpN
  , fctLumpMin
  , fctLumpProp
  , fctLumpLowfreq
  ) where

import           Data.List (foldl', sort, sortBy, elemIndex)
import           Data.Maybe (mapMaybe)
import           Data.Ord (comparing, Down (..))
import qualified Data.Map.Strict as M
import           Data.Text (Text)
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as VU

-- === 型 =====================================================================

-- | 因子。 水準ラベル + 各観測の水準コード (0 始まり・NA は 'naCode')。
data Factor = Factor
  { facLevels  :: [Text]        -- ^ 水準ラベル (定義順)
  , facCodes   :: VU.Vector Int -- ^ 各観測の水準コード (0 始まり・NA = 'naCode')
  , facOrdered :: Bool          -- ^ 順序付き因子か (R `ordered()`)
  } deriving (Eq, Show)

-- | 欠損コード (R の @NA_integer_@ 相当)。
naCode :: Int
naCode = -1

-- === 生成 ===================================================================

-- | @factor xs@ : R の `factor()` 既定。 水準 = 値の **ソート済 unique**。
factor :: [Text] -> Factor
factor xs = factorWith (sortUnique xs) xs

-- | @factorWith lvls xs@ : 水準を明示。 @lvls@ に無い値は NA ('naCode')。
factorWith :: [Text] -> [Text] -> Factor
factorWith lvls xs = Factor lvls (VU.fromList (map enc xs)) False
  where
    idx   = M.fromList (zip lvls [0 ..])
    enc x = M.findWithDefault naCode x idx

-- | @fct xs@ : forcats の `fct()`。 水準 = 値の **出現順 unique** (factor() と違い sort しない)。
fct :: [Text] -> Factor
fct xs = factorWith (nubKeepOrder xs) xs

-- | @ordered lvls xs@ : 順序付き因子 (R `ordered()`)。 水準間に @<@ 順序を持つ。
ordered :: [Text] -> [Text] -> Factor
ordered lvls xs = (factorWith lvls xs) { facOrdered = True }

-- === 参照 ===================================================================

-- | 水準ラベル (定義順)。
levels :: Factor -> [Text]
levels = facLevels

-- | 順序付き因子か。
isOrdered :: Factor -> Bool
isOrdered = facOrdered

-- | @as.character()@ 相当。 各観測をラベルへ。 NA は @""@。
asTexts :: Factor -> [Text]
asTexts f = map (maybe "" id) (asTextsMaybe f)

-- | NA を 'Nothing' で残す版。
asTextsMaybe :: Factor -> [Maybe Text]
asTextsMaybe f = map lab (VU.toList (facCodes f))
  where
    v = V.fromList (facLevels f)
    lab c | c < 0 || c >= V.length v = Nothing
          | otherwise                = Just (v V.! c)

-- | dplyr `count()` 相当。 **水準順**に (ラベル, 出現数)。 0 件水準も含む。 NA は除外。
fctCount :: Factor -> [(Text, Int)]
fctCount f = [ (lv, M.findWithDefault 0 i cmap) | (lv, i) <- zip (facLevels f) [0 ..] ]
  where
    cmap = foldl' (\m c -> if c < 0 then m else M.insertWith (+) c 1 m)
                  M.empty (VU.toList (facCodes f))

-- === 順序操作 (16.4) ========================================================
--
-- いずれも **facLevels の順序を入れ替え、 facCodes を旧 code → 新 code に
-- 再マッピング**して実装する ('applyLevelOrder')。 観測の中身 (どの水準か) は
-- 不変で、 水準の**並び順**だけが変わる (forcats `fct_reorder` 系と同義)。

-- | forcats `fct_reorder(f, x, .fun)`。 各水準に属する @x@ 値へ集約関数 @fun@
-- (R4DS の既定は @median@) を適用し、 その値の**昇順**に水準を並べ替える。
-- @x@ 中の @NaN@ は NA とみなし集約前に除去。 値が無い水準は末尾。
fctReorder :: ([Double] -> Double) -> Factor -> [Double] -> Factor
fctReorder fun f xs = applyLevelOrder order f
  where
    n     = length (facLevels f)
    grp   = groupByCode n (facCodes f) xs
    key i = case filter (not . isNaN) (grp V.! i) of
              [] -> 1 / 0          -- 値なし → +Inf で末尾へ
              vs -> fun vs
    order = sortBy (comparing key) [0 .. n - 1]

-- | forcats `fct_relevel(f, ...)`。 指定した水準を (指定順で) **先頭**へ移し、
-- 残りは元の相対順序を保つ。 存在しない水準名は無視する。
fctRelevel :: [Text] -> Factor -> Factor
fctRelevel front f = applyLevelOrder order f
  where
    lvs      = facLevels f
    frontIx  = mapMaybe (`elemIndex` lvs) front
    rest     = [ i | i <- [0 .. length lvs - 1], i `notElem` frontIx ]
    order    = frontIx ++ rest

-- | forcats `fct_reorder2(f, x, y)` (既定 @last2@)。 各水準で **最大の @x@ に
-- 対応する @y@** を取り、 その値の**降順**に水準を並べ替える (凡例順を線の
-- 右端の高さに合わせる用途)。 @x@ 同値は後着 (入力順で後ろ) を採用。
fctReorder2 :: Factor -> [Double] -> [Double] -> Factor
fctReorder2 f xs ys = applyLevelOrder order f
  where
    n     = length (facLevels f)
    grp   = groupPairs n (facCodes f) xs ys
    key i = case grp V.! i of
              [] -> -1 / 0         -- 値なし → -Inf で降順末尾へ
              ps -> snd (foldl1 (\a b -> if fst b >= fst a then b else a) ps)
    order = sortBy (comparing (Down . key)) [0 .. n - 1]

-- | forcats `fct_infreq(f)`。 出現**頻度の降順**に水準を並べ替える。
-- 同頻度は元の水準順を保つ (安定)。 NA は計数対象外。
fctInfreq :: Factor -> Factor
fctInfreq f = applyLevelOrder order f
  where
    n      = length (facLevels f)
    counts = [ M.findWithDefault 0 i cmap | i <- [0 .. n - 1] ] :: [Int]
    cmap   = foldl' (\m c -> if c < 0 then m else M.insertWith (+) c 1 m)
                    M.empty (VU.toList (facCodes f))
    order  = sortBy (comparing (Down . (countArr V.!))) [0 .. n - 1]
    countArr = V.fromList counts

-- | forcats `fct_rev(f)`。 水準を**逆順**にする。
fctRev :: Factor -> Factor
fctRev f = applyLevelOrder (reverse [0 .. length (facLevels f) - 1]) f

-- === 水準操作 (16.5) ========================================================
--
-- 水準**ラベルそのもの**を付け替える/併合する操作。 ラベルを付け替えた結果
-- 同名になった水準は 1 つに畳む ('relabelMerge')。 lump 系は余りを @"Other"@ に
-- まとめ、 forcats と同じく @"Other"@ を**末尾水準**に置く。

-- | 余り水準のまとめ先ラベル (forcats `other_level` 既定)。
otherLevel :: Text
otherLevel = "Other"

-- | forcats `fct_recode(f, new = "old", ...)`。 水準ラベルを改名する。
-- 引数は @(新, 旧)@ の対。 複数の旧を同じ新に向ければ**併合**される。
-- 言及されない水準はそのまま。 水準の相対順序は保つ。
fctRecode :: [(Text, Text)] -> Factor -> Factor
fctRecode pairs f = relabelMerge newLabels f
  where
    m         = M.fromList [ (old, new) | (new, old) <- pairs ]
    newLabels = [ M.findWithDefault lv lv m | lv <- facLevels f ]

-- | forcats `fct_collapse(f, new = c("o1","o2"), ...)`。 複数水準を 1 つへ併合。
-- 引数は @(新, [旧...])@。 言及されない水準はそのまま (`other_level` は非対応)。
fctCollapse :: [(Text, [Text])] -> Factor -> Factor
fctCollapse groups f = relabelMerge newLabels f
  where
    m         = M.fromList [ (old, new) | (new, olds) <- groups, old <- olds ]
    newLabels = [ M.findWithDefault lv lv m | lv <- facLevels f ]

-- | forcats `fct_lump_n(f, n)`。 頻度上位 @n@ 水準を残し、 他を @"Other"@ へ。
-- @n@ 負なら頻度**下位** @|n|@ を残す。 同頻度のタイは両方残す (上位 n 側)。
fctLumpN :: Int -> Factor -> Factor
fctLumpN n f
  | m == 0    = f
  | n == 0    = lumpBy (const True) f
  | n > 0     = lumpBy (\i -> cnts !! i < thrTop) f
  | otherwise = lumpBy (\i -> cnts !! i > thrBot) f
  where
    cnts   = levelCounts f
    m      = length cnts
    thrTop | n >= m    = minimum cnts                      -- 全水準保持
           | otherwise = sortBy (comparing Down) cnts !! (n - 1)
    k      = negate n
    thrBot | k >= m    = maximum cnts                      -- 全水準保持
           | otherwise = sort cnts !! (k - 1)

-- | forcats `fct_lump_min(f, min)`。 出現回数 @< min@ の水準を @"Other"@ へ (strict)。
fctLumpMin :: Int -> Factor -> Factor
fctLumpMin k f = lumpBy (\i -> cnts !! i < k) f
  where cnts = levelCounts f

-- | forcats `fct_lump_prop(f, prop)`。 出現割合 @< prop@ の水準を @"Other"@ へ。
fctLumpProp :: Double -> Factor -> Factor
fctLumpProp p f = lumpBy (\i -> fromIntegral (cnts !! i) < p * fromIntegral total) f
  where
    cnts  = levelCounts f
    total = sum cnts

-- | forcats `fct_lump_lowfreq(f)`。 低頻度水準を、 @"Other"@ が**最小水準のまま**で
-- いられる範囲で併合する。 頻度降順に並べ、 ある水準が「それより低頻度な水準の
-- 合計」 を上回った時点で、 以降を @"Other"@ へまとめる (forcats `lump_cutoff` 同型)。
fctLumpLowfreq :: Factor -> Factor
fctLumpLowfreq f
  | m == 0    = f
  | otherwise = lumpBy (\i -> (cnts !! i, i) `elem` lumpedKeys) f
  where
    cnts       = levelCounts f
    m          = length cnts
    -- (頻度, 水準index) を頻度降順 (タイは index 昇順) に
    descKeys   = sortBy (comparing (\(c, i) -> (Down c, i))) (zip cnts [0 ..])
    cut        = cutoff (map fst descKeys)          -- 保持する個数 (cut..末尾を lump)
    lumpedKeys = drop cut descKeys

-- | forcats `lump_cutoff`: 降順 count 列で、 「自分 > 残り (より低頻度) の合計」 と
-- なる最初の位置 @i@ を見つけ、 @i+1@ (= 0 始まりで @i@ 番目まで保持) を返す。
-- 該当無しなら全保持 (= 長さ)。
cutoff :: [Int] -> Int
cutoff xs = go 0 (sum xs) xs
  where
    go i _    []       = i
    go i left (c : cs)
      | c > left - c   = i + 1
      | otherwise      = go (i + 1) (left - c) cs

-- === 内部 helper ============================================================

-- | 水準ラベルを @newLabels@ (旧水準 index 順・長さ = 旧水準数) に付け替え、
-- 同名になった水準を 1 つに畳む。 新水準順は新ラベルの**初出順**。 NA は不変。
relabelMerge :: [Text] -> Factor -> Factor
relabelMerge newLabels f =
  Factor newLevels (VU.map remap (facCodes f)) (facOrdered f)
  where
    newLevels = nubKeepOrder newLabels
    pos       = M.fromList (zip newLevels [0 ..])
    o2nVec    = V.fromList [ pos M.! lab | lab <- newLabels ]
    remap c | c < 0 || c >= V.length o2nVec = c
            | otherwise                     = o2nVec V.! c

-- | 指定水準を**末尾**へ移す (存在しなければ不変)。 'applyLevelOrder' を再利用。
moveLevelToEnd :: Text -> Factor -> Factor
moveLevelToEnd lv f = case elemIndex lv (facLevels f) of
  Nothing -> f
  Just i  -> applyLevelOrder ([ j | j <- [0 .. n - 1], j /= i ] ++ [i]) f
  where n = length (facLevels f)

-- | 水準 index で lump 判定し、 余りを 'otherLevel' へ併合 (末尾に置く)。
-- lump 対象が無ければ不変。
lumpBy :: (Int -> Bool) -> Factor -> Factor
lumpBy isLump f
  | not (or lumpFlags) = f
  | otherwise          = moveLevelToEnd otherLevel (relabelMerge newLabels f)
  where
    lumpFlags = map isLump [0 .. length (facLevels f) - 1]
    newLabels = [ if isLump i then otherLevel else lv
                | (i, lv) <- zip [0 ..] (facLevels f) ]

-- | 各水準 (定義順) の出現回数。 NA は除外。
levelCounts :: Factor -> [Int]
levelCounts f = [ M.findWithDefault 0 i cmap | i <- [0 .. length (facLevels f) - 1] ]
  where
    cmap = foldl' (\m c -> if c < 0 then m else M.insertWith (+) c 1 m)
                  M.empty (VU.toList (facCodes f))

-- | @applyLevelOrder order f@ : @order@ は新しい並びを表す**旧 index のリスト**
-- (@order !! p@ = 新位置 @p@ に来る旧水準 index)。 facLevels を並べ替え、
-- facCodes を旧 code → 新 code に再マップする。 NA ('naCode') はそのまま。
applyLevelOrder :: [Int] -> Factor -> Factor
applyLevelOrder order f = f
  { facLevels = map (lvs V.!) order
  , facCodes  = VU.map remap (facCodes f)
  }
  where
    lvs   = V.fromList (facLevels f)
    n     = length (facLevels f)
    -- 旧 index → 新 index
    o2n   = VU.replicate n (-1) VU.// [ (old, new) | (new, old) <- zip [0 ..] order ]
    remap c | c < 0 || c >= n = c
            | otherwise       = o2n VU.! c

-- | 水準コードで @x@ 値をグループ化 (長さ n・入力順保持・NA/範囲外は除外)。
groupByCode :: Int -> VU.Vector Int -> [Double] -> V.Vector [Double]
groupByCode n codes xs =
  V.map reverse $ V.accum (flip (:)) (V.replicate n [])
    [ (c, x) | (c, x) <- zip (VU.toList codes) xs, c >= 0, c < n ]

-- | 水準コードで @(x, y)@ 対をグループ化 (長さ n・入力順保持・NA/範囲外は除外)。
groupPairs :: Int -> VU.Vector Int -> [Double] -> [Double] -> V.Vector [(Double, Double)]
groupPairs n codes xs ys =
  V.map reverse $ V.accum (flip (:)) (V.replicate n [])
    [ (c, (x, y)) | (c, x, y) <- zip3 (VU.toList codes) xs ys, c >= 0, c < n ]

-- | ソート済 unique (Map のキー集合 = 昇順 unique)。
sortUnique :: [Text] -> [Text]
sortUnique = M.keys . M.fromList . map (\x -> (x, ()))

-- | 出現順を保った unique。
nubKeepOrder :: [Text] -> [Text]
nubKeepOrder = go M.empty
  where
    go _ [] = []
    go seen (x : xs)
      | x `M.member` seen = go seen xs
      | otherwise         = x : go (M.insert x () seen) xs