packages feed

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

{-# LANGUAGE ScopedTypeVariables #-}
-- |
-- Module      : Hanalyze.Data.Transform
-- Description : dplyr 流の順位・オフセット・累積・区間化を純粋な [a] -> [b] として提供
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- dplyr 流の順序付き/窓関数的なベクトル変換 (Phase 66)。
--
-- R4DS Ch13 "Numbers" で扱う順位・オフセット・累積・区間化・連続識別子を、
-- **純粋な `[a] -> [b]`** として公開する。 統計 ('Stat.Descriptive') でも IO/DataFrame
-- ('DataIO') でもなく、 純粋データ抽象の `Data/` 名前空間に置く ('Data.ColumnSource'
-- の隣)。 DataFrame 直結解析 API (Phase 67) の @mutate@ もこれを呼ぶ。
--
-- === NA
-- 順位関数は NA を含むベクトル用に @*NA@ 変種 (@[Maybe a] -> [Maybe b]@・dplyr の
-- @na.last="keep"@ と同じく Nothing は Nothing を保ち、 残りを順位付け) を併設する。
--
-- === desc
-- 降順順位は別関数を設けず 'Data.Ord.Down' を被せる (@minRank (map Down xs)@・
-- NA 付きは @minRankNA (map (fmap Down) xs)@)。
module Hanalyze.Data.Transform
  ( -- * 順位 (dplyr ranking)
    minRank, denseRank, rowNumber
  , percentRank, cumeDist
    -- * 順位 (NA 保持変種)
  , minRankNA, denseRankNA, rowNumberNA
  , percentRankNA, cumeDistNA
    -- * オフセット
  , lag, lead
    -- * 累積
  , cumsum, cumprod, cummin, cummax, cummean
    -- * 区間化
  , cut, cutLabels
    -- * 連続識別子
  , consecutiveId
  ) where

import           Data.List       (sortOn)
import qualified Data.Map.Strict as M
import qualified Data.Set        as S

-- ===========================================================================
-- 順位
-- ===========================================================================

-- | 最小順位法 (dplyr @min_rank@・tie は最小を共有し次を飛ばす: 1,2,2,4)。
--   各値の順位 = 1 + (厳密に小さい要素数)。
minRank :: Ord a => [a] -> [Int]
minRank xs = map ((rm M.!) ) xs
  where
    sorted = sortOn id xs
    -- 値 → 最初の出現 index (= 厳密に小さい要素数)。1 始まりにして格納。
    rm = M.fromListWith min [ (v, i + 1) | (i, v) <- zip [0 ..] sorted ]

-- | 密順位法 (dplyr @dense_rank@・tie で番号を飛ばさない: 1,2,2,3)。
--   各値の順位 = その値以下の **相異なる値の個数**。
denseRank :: Ord a => [a] -> [Int]
denseRank xs = map (dm M.!) xs
  where
    distinct = S.toAscList (S.fromList xs)
    dm = M.fromList (zip distinct [1 :: Int ..])

-- | 行番号 (dplyr @row_number@・tie も出現順で一意: 1,2,3,4)。
rowNumber :: Ord a => [a] -> [Int]
rowNumber xs = map (rm M.!) [0 .. n - 1]
  where
    n     = length xs
    order = map fst (sortOn snd (zip [0 :: Int ..] xs))  -- (origIdx, value) を value, origIdx 昇順
    rm    = M.fromList (zip order [1 :: Int ..])

-- | パーセント順位 (dplyr @percent_rank@ = (minRank - 1)/(n - 1))。
percentRank :: Ord a => [a] -> [Double]
percentRank xs
  | n <= 1    = map (const 0) xs
  | otherwise = [ fromIntegral (r - 1) / fromIntegral (n - 1) | r <- minRank xs ]
  where n = length xs

-- | 累積分布 (dplyr @cume_dist@ = (≤x の個数)/n)。
cumeDist :: Ord a => [a] -> [Double]
cumeDist xs = map (\x -> fromIntegral (cm M.! x) / fromIntegral n) xs
  where
    n      = length xs
    sorted = sortOn id xs
    -- 値 → その値の最後の出現 index + 1 (= ≤ その値の個数)。
    cm = M.fromListWith max [ (v, i + 1) | (i, v) <- zip [0 ..] sorted ]

-- --- NA 保持変種 -----------------------------------------------------------

-- | 非 NA だけを @f@ で順位付けし、 NA (Nothing) は Nothing のまま位置を保つ。
onJusts :: forall a b. ([a] -> [b]) -> [Maybe a] -> [Maybe b]
onJusts f xs =
  let idxVals = [ (i, a) | (i, Just a) <- zip [0 ..] xs ]
      ranked  = f (map snd idxVals)
      m       = M.fromList (zip (map fst idxVals) ranked)
  in [ M.lookup i m | i <- [0 .. length xs - 1] ]

minRankNA     :: Ord a => [Maybe a] -> [Maybe Int]
minRankNA      = onJusts minRank
denseRankNA   :: Ord a => [Maybe a] -> [Maybe Int]
denseRankNA    = onJusts denseRank
rowNumberNA   :: Ord a => [Maybe a] -> [Maybe Int]
rowNumberNA    = onJusts rowNumber
percentRankNA :: Ord a => [Maybe a] -> [Maybe Double]
percentRankNA  = onJusts percentRank
cumeDistNA    :: Ord a => [Maybe a] -> [Maybe Double]
cumeDistNA     = onJusts cumeDist

-- ===========================================================================
-- オフセット
-- ===========================================================================

-- | @lag n d xs@: 各値を n 個後ろへずらし、 先頭 n 個を default @d@ で埋める
--   (dplyr @lag(x, n, default)@・既定 R は NA)。 入力と同長。
lag :: Int -> a -> [a] -> [a]
lag n d xs = take (length xs) (replicate n d ++ xs)

-- | @lead n d xs@: 各値を n 個前へずらし、 末尾 n 個を default @d@ で埋める。
lead :: Int -> a -> [a] -> [a]
lead n d xs = take (length xs) (drop n xs ++ repeat d)

-- ===========================================================================
-- 累積
-- ===========================================================================

cumsum  :: Num a => [a] -> [a]
cumsum   = scanl1 (+)
cumprod :: Num a => [a] -> [a]
cumprod  = scanl1 (*)
cummin  :: Ord a => [a] -> [a]
cummin   = scanl1 min
cummax  :: Ord a => [a] -> [a]
cummax   = scanl1 max

-- | 累積平均 (dplyr @cummean@・i 番目 = 先頭 i 個の平均)。
cummean :: [Double] -> [Double]
cummean xs = zipWith (\s i -> s / fromIntegral i) (scanl1 (+) xs) [1 :: Int ..]

-- ===========================================================================
-- 区間化 (base R cut・既定 right = TRUE → (a, b])
-- ===========================================================================

-- | @cut breaks xs@: 各値が属する bin の index (1 始まり) を返す。 境界は昇順前提・
--   区間は @(lo, hi]@ (right=TRUE)・範囲外は Nothing (= R の NA)。
cut :: [Double] -> [Double] -> [Maybe Int]
cut breaks = map binOf
  where
    intervals = zip [1 :: Int ..] (zip breaks (drop 1 breaks))
    binOf x = case [ i | (i, (lo, hi)) <- intervals, x > lo, x <= hi ] of
                (i : _) -> Just i
                []      -> Nothing

-- | ラベル付き 'cut' (@labels@ は @breaks - 1@ 個)。
cutLabels :: [b] -> [Double] -> [Double] -> [Maybe b]
cutLabels labels breaks = map (fmap (labels !!) . fmap (subtract 1)) . cut breaks

-- ===========================================================================
-- 連続識別子 (dplyr consecutive_id・値が変わるたび +1)
-- ===========================================================================

consecutiveId :: Eq a => [a] -> [Int]
consecutiveId = go Nothing 0
  where
    go _ _ [] = []
    go prev cur (y : ys) =
      let cur' = case prev of
                   Just p | p == y -> cur
                   _               -> cur + 1
      in cur' : go (Just y) cur' ys