packages feed

hanalyze-frame-0.2.0.1: 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) 型操作 (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')。
--
-- [English]: forcats-style factor type operations (Ch16 "Factors").
--
-- Provides the 'Factor' type, which represents R's `factor` as
-- __an ordered list of levels plus each observation's level code__, along with pure
-- functions for the `fct_*` equivalents from forcats (a pure `Data/`
-- abstraction alongside 'Data.Strings' \/ 'Data.Transform').
--
-- === Why a dedicated type
-- Unlike a plain @[Text]@, a factor carries (1) a __semantic ordering__ of
-- levels (distinct from alphabetical order), (2) levels that persist even
-- when absent from the data, and (3) integer coding. forcats's `fct_*`
-- functions manipulate this level ordering and content, which has no
-- counterpart for an order-less @[Text]@.
--
-- === Difference from HBM's Column.Factor
-- The internal @Column = Numeric | Factor@ in 'Hanalyze.Model.HBM' is
-- the internal representation of an observation column passed to NUTS. This
-- 'Factor' is a public type for the __data-wrangling domain__ with a
-- separate responsibility (an independent implementation).
--
-- === Coding convention
-- 'facCodes' is __0-indexed__ (recovered via @facLevels !! code@). Missing
-- values (R's @\<NA\>@) are represented by code @-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')。
--   [English]: A factor. Level labels + each observation's level code
--   (0-indexed; NA is 'naCode').
data Factor = Factor
  { facLevels  :: [Text]        -- ^ [日本語]: 水準ラベル (定義順) [English]: The level labels, in definition order.
  , facCodes   :: VU.Vector Int -- ^ [日本語]: 各観測の水準コード (0 始まり・NA = 'naCode') [English]: Each observation's level code (0-indexed; NA = 'naCode').
  , facOrdered :: Bool          -- ^ [日本語]: 順序付き因子か (R `ordered()`) [English]: Whether this is an ordered factor (R's `ordered()`).
  } deriving (Eq, Show)

-- | [日本語]: 欠損コード (R の @NA_integer_@ 相当)。
--   [English]: The missing-value code (equivalent to R's @NA_integer_@).
naCode :: Int
naCode = -1

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

-- | [日本語]: @factor xs@ : R の `factor()` 既定。 水準 = 値の __ソート済 unique__。
--   [English]: @factor xs@: R's default `factor()`. The levels are the
--   __sorted unique__ values.
factor :: [Text] -> Factor
factor xs = factorWith (sortUnique xs) xs

-- | [日本語]: @factorWith lvls xs@ : 水準を明示。 @lvls@ に無い値は NA ('naCode')。
--   [English]: @factorWith lvls xs@: Levels are given explicitly. Values not
--   present in @lvls@ become 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 しない)。
--   [English]: @fct xs@: forcats's `fct()`. The levels are the values'
--   __unique values in order of first appearance__ (unlike factor(), it
--   does not sort).
fct :: [Text] -> Factor
fct xs = factorWith (nubKeepOrder xs) xs

-- | [日本語]: @ordered lvls xs@ : 順序付き因子 (R `ordered()`)。 水準間に @<@ 順序を持つ。
--   [English]: @ordered lvls xs@: An ordered factor (R's `ordered()`), with
--   a @<@ ordering between levels.
ordered :: [Text] -> [Text] -> Factor
ordered lvls xs = (factorWith lvls xs) { facOrdered = True }

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

-- | [日本語]: 水準ラベル (定義順)。
--   [English]: The level labels, in definition order.
levels :: Factor -> [Text]
levels = facLevels

-- | [日本語]: 順序付き因子か。
--   [English]: Whether this is an ordered factor.
isOrdered :: Factor -> Bool
isOrdered = facOrdered

-- | [日本語]: @as.character()@ 相当。 各観測をラベルへ。 NA は @""@。
--   [English]: Equivalent to @as.character()@. Converts each observation to
--   its label; NA becomes @""@.
asTexts :: Factor -> [Text]
asTexts f = map (maybe "" id) (asTextsMaybe f)

-- | [日本語]: NA を 'Nothing' で残す版。
--   [English]: The variant that keeps NA as '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 は除外。
--   [English]: Equivalent to dplyr's `count()`. Returns (label, count) pairs
--   __in level order__, including levels with zero occurrences. NA is
--   excluded.
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 とみなし集約前に除去。 値が無い水準は末尾。
--   [English]: forcats's `fct_reorder(f, x, .fun)`. Applies the aggregation
--   function @fun@ (R4DS's default is @median@) to the @x@ values belonging
--   to each level, then reorders the levels by that value's __ascending__
--   order. @NaN@ in @x@ is treated as NA and removed before aggregation.
--   Levels with no values go last.
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, ...)`。 指定した水準を (指定順で) __先頭__へ移し、
-- 残りは元の相対順序を保つ。 存在しない水準名は無視する。
--   [English]: forcats's `fct_relevel(f, ...)`. Moves the given levels
--   (in the given order) to the __front__, keeping the rest in their
--   original relative order. Nonexistent level names are ignored.
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@ 同値は後着 (入力順で後ろ) を採用。
--   [English]: forcats's `fct_reorder2(f, x, y)` (default @last2@). Takes,
--   for each level, __the @y@ corresponding to the largest @x@__, and
--   reorders levels by that value's __descending__ order (used to align
--   legend order with the height at the line's right edge). Ties in @x@
--   favor the later-occurring input.
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 は計数対象外。
--   [English]: forcats's `fct_infreq(f)`. Reorders levels by
--   __descending frequency of occurrence__. Ties keep the original level order (stable);
--   NA is excluded from counting.
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)`。 水準を__逆順__にする。
--   [English]: forcats's `fct_rev(f)`. Reverses the order of the levels.
fctRev :: Factor -> Factor
fctRev f = applyLevelOrder (reverse [0 .. length (facLevels f) - 1]) f

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

-- | [日本語]: 余り水準のまとめ先ラベル (forcats @other_level@ 既定)。
--   [English]: The label that leftover levels are merged into (forcats's
--   default @other_level@).
otherLevel :: Text
otherLevel = "Other"

-- | [日本語]: forcats `fct_recode(f, new = "old", ...)`。 水準ラベルを改名する。
-- 引数は @(新, 旧)@ の対。 複数の旧を同じ新に向ければ__併合__される。
-- 言及されない水準はそのまま。 水準の相対順序は保つ。
--   [English]: forcats's `fct_recode(f, new = "old", ...)`. Renames level
--   labels. Arguments are @(new, old)@ pairs; pointing multiple olds at the
--   same new __merges__ them. Unmentioned levels are left unchanged, and
--   the levels' relative order is preserved.
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@ は非対応)。
--   [English]: forcats's `fct_collapse(f, new = c("o1","o2"), ...)`. Merges
--   multiple levels into one. Arguments are @(new, [old...])@. Unmentioned
--   levels are left unchanged (@other_level@ is not supported).
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 側)。
--   [English]: forcats's `fct_lump_n(f, n)`. Keeps the top-@n@ levels by
--   frequency, lumping the rest into @"Other"@. If @n@ is negative, keeps
--   the bottom @|n|@ by frequency. Ties at the frequency threshold are both
--   kept (on the top-n side).
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)。
--   [English]: forcats's `fct_lump_min(f, min)`. Lumps levels with a count
--   @< min@ into @"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"@ へ。
--   [English]: forcats's `fct_lump_prop(f, prop)`. Lumps levels whose
--   proportion of occurrences is @< prop@ into @"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@ 同型)。
--   [English]: forcats's `fct_lump_lowfreq(f)`. Merges low-frequency levels
--   to the extent that @"Other"@ can remain __the smallest level__. Levels
--   are ordered by descending frequency, and once a level's count exceeds
--   "the sum of all less-frequent levels", everything from there on is
--   lumped into @"Other"@ (isomorphic to forcats's @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@ 番目まで保持) を返す。
-- 該当無しなら全保持 (= 長さ)。
--   [English]: forcats's @lump_cutoff@. In a descending-count list, finds
--   the first position @i@ where "this count > the sum of the remaining
--   (less-frequent) counts", and returns @i+1@ (i.e. keep through 0-indexed
--   position @i@). If no such position exists, keeps everything (= the
--   length).
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 は不変。
--   [English]: Relabels the levels using @newLabels@ (in old-level index
--   order; length = number of old levels), folding levels that become
--   identically named into one. The new level order follows the new
--   labels' __order of first appearance__. NA is unchanged.
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' を再利用。
--   [English]: Moves the given level to the __end__ (a no-op if it doesn't
--   exist). Reuses '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 対象が無ければ不変。
--   [English]: Decides lumping by level index, merging the leftovers into
-- 'otherLevel' (placed at the end). A no-op if nothing qualifies for
-- lumping.
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 は除外。
--   [English]: The occurrence count of each level, in definition order.
--   NA is excluded.
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') はそのまま。
--   [English]: @applyLevelOrder order f@: @order@ is
--   __a list of old indices__ representing the new arrangement (@order !! p@ = the old
--   level index that lands at new position @p@). Reorders facLevels and
--   remaps facCodes from old code to new code. NA ('naCode') is left
--   unchanged.
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/範囲外は除外)。
--   [English]: Groups @x@ values by level code (length n, preserving input
--   order; NA \/ out-of-range are excluded).
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/範囲外は除外)。
--   [English]: Groups @(x, y)@ pairs by level code (length n, preserving
--   input order; NA \/ out-of-range are excluded).
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)。
--   [English]: Sorted unique values (a Map's key set = ascending unique).
sortUnique :: [Text] -> [Text]
sortUnique = M.keys . M.fromList . map (\x -> (x, ()))

-- | [日本語]: 出現順を保った unique。
--   [English]: Unique values that preserve order of appearance.
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