diff --git a/dataframe-core.cabal b/dataframe-core.cabal
--- a/dataframe-core.cabal
+++ b/dataframe-core.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.4
 name:               dataframe-core
-version:            2.5.0.1
+version:            2.5.0.2
 synopsis:           Core data structures for the dataframe library.
 description:
     Minimal interchange-format types for the @dataframe@ ecosystem:
diff --git a/src-internal/DataFrame/Internal/Column/Conversion.hs b/src-internal/DataFrame/Internal/Column/Conversion.hs
--- a/src-internal/DataFrame/Internal/Column/Conversion.hs
+++ b/src-internal/DataFrame/Internal/Column/Conversion.hs
@@ -411,10 +411,15 @@
         Nothing -> case testEquality (typeRep @a) (typeRep @T.Text) of
             Just Refl -> col'
             Nothing -> VB.map (T.pack . show) col'
-        Just bitmap ->
-            VB.imap
-                (\i x -> if bitmapTestBit bitmap i then T.pack (show x) else "null")
-                col'
+        Just bitmap -> case testEquality (typeRep @a) (typeRep @T.Text) of
+            Just Refl ->
+                VB.imap
+                    (\i x -> if bitmapTestBit bitmap i then x else "null")
+                    col'
+            Nothing ->
+                VB.imap
+                    (\i x -> if bitmapTestBit bitmap i then T.pack (show x) else "null")
+                    col'
 columnToTextVec (UnboxedColumn bm col') =
     case bm of
         Nothing -> VB.map (T.pack . show) (VB.convert col')
diff --git a/src-internal/DataFrame/Internal/Column/Encode.hs b/src-internal/DataFrame/Internal/Column/Encode.hs
--- a/src-internal/DataFrame/Internal/Column/Encode.hs
+++ b/src-internal/DataFrame/Internal/Column/Encode.hs
@@ -126,8 +126,8 @@
         ht <- newHashTable (min n (maxCard + 1))
         codes <- VUM.new n
         let go !i !next
-                | i >= n = pure (Just next)
                 | next > maxCard = pure Nothing
+                | i >= n = pure (Just next)
                 | otherwise = do
                     let !h = hashAt i
                     (code, isNew) <- htInsert ht eqAt next i h
diff --git a/src-internal/DataFrame/Internal/Column/Operations.hs b/src-internal/DataFrame/Internal/Column/Operations.hs
--- a/src-internal/DataFrame/Internal/Column/Operations.hs
+++ b/src-internal/DataFrame/Internal/Column/Operations.hs
@@ -801,23 +801,26 @@
 zipColumns l r@(MergedColumn _ _) = zipColumns l (materializeMerged r)
 zipColumns l@(PackedText _ _) r = zipColumns (materializePacked l) r
 zipColumns l r@(PackedText _ _) = zipColumns l (materializePacked r)
-zipColumns (BoxedColumn _ column) (BoxedColumn _ other) = BoxedColumn Nothing (VG.zip column other)
-zipColumns (BoxedColumn _ column) (UnboxedColumn _ other) =
-    BoxedColumn
-        Nothing
-        ( VB.generate
-            (min (VG.length column) (VG.length other))
-            (\i -> (column VG.! i, other VG.! i))
-        )
-zipColumns (UnboxedColumn _ column) (BoxedColumn _ other) =
-    BoxedColumn
-        Nothing
-        ( VB.generate
-            (min (VG.length column) (VG.length other))
-            (\i -> (column VG.! i, other VG.! i))
-        )
-zipColumns (UnboxedColumn _ column) (UnboxedColumn _ other) = UnboxedColumn Nothing (VG.zip column other)
+zipColumns (BoxedColumn _ column) (BoxedColumn _ other) = zipVectors column other
+zipColumns (BoxedColumn _ column) (UnboxedColumn _ other) = zipVectors column other
+zipColumns (UnboxedColumn _ column) (BoxedColumn _ other) = zipVectors column other
+zipColumns (UnboxedColumn _ column) (UnboxedColumn _ other) = zipVectors column other
 {-# INLINE zipColumns #-}
+
+zipVectors ::
+    forall a b va vb.
+    (Columnable a, Columnable b, VG.Vector va a, VG.Vector vb b) =>
+    va a -> vb b -> Column
+zipVectors column other =
+    case sUnbox @a of
+        STrue -> case sUnbox @b of
+            STrue -> UnboxedColumn Nothing (VU.generate n pairAt)
+            SFalse -> BoxedColumn Nothing (VB.generate n pairAt)
+        SFalse -> BoxedColumn Nothing (VB.generate n pairAt)
+  where
+    !n = min (VG.length column) (VG.length other)
+    pairAt i = (VG.unsafeIndex column i, VG.unsafeIndex other i)
+{-# INLINE zipVectors #-}
 
 -- | An internal, column version of zipWith.
 zipWithColumns ::
diff --git a/src-internal/DataFrame/Internal/Column/Types.hs b/src-internal/DataFrame/Internal/Column/Types.hs
--- a/src-internal/DataFrame/Internal/Column/Types.hs
+++ b/src-internal/DataFrame/Internal/Column/Types.hs
@@ -58,6 +58,7 @@
     Unboxable Bool = 'True
     Unboxable Double = 'True
     Unboxable Float = 'True
+    Unboxable (a, b) = If (Unboxable a) (Unboxable b) 'False
     Unboxable _ = 'False
 
 type family Numeric (a :: Type) :: Bool where
diff --git a/src-internal/DataFrame/Internal/Expression.hs b/src-internal/DataFrame/Internal/Expression.hs
--- a/src-internal/DataFrame/Internal/Expression.hs
+++ b/src-internal/DataFrame/Internal/Expression.hs
@@ -71,9 +71,6 @@
     binaryCommutative (MkBinaryOp{binaryCommutative = c}) = c
     binaryPrecedence (MkBinaryOp{binaryPrecedence = p}) = p
 
-data MeanAcc = MeanAcc {-# UNPACK #-} !Double {-# UNPACK #-} !Int
-    deriving (Show, Eq, Ord, Read)
-
 data AggStrategy a b where
     CollectAgg ::
         (VG.Vector v b, Typeable v) => T.Text -> (v b -> a) -> AggStrategy a b
@@ -361,26 +358,24 @@
     eqNormalized _ _ = False
 
 replaceExpr ::
-    forall a b c.
-    (Columnable a, Columnable b, Columnable c) =>
-    Expr a -> Expr b -> Expr c -> Expr c
-replaceExpr new old expr = case testEquality (typeRep @b) (typeRep @c) of
-    Just Refl -> case testEquality (typeRep @a) (typeRep @c) of
-        Just Refl -> if eqExpr old expr then new else replace'
-        Nothing -> expr
+    forall a b.
+    (Columnable a, Columnable b) =>
+    Expr a -> Expr a -> Expr b -> Expr b
+replaceExpr old new expr = case testEquality (typeRep @a) (typeRep @b) of
+    Just Refl -> if eqExpr old expr then new else replace'
     Nothing -> replace'
   where
     replace' = case expr of
         (Col _) -> expr
         (CastWith{}) -> expr
-        (CastExprWith t f e) -> CastExprWith t f (replaceExpr new old e)
+        (CastExprWith t f e) -> CastExprWith t f (replaceExpr old new e)
         (Lit _) -> expr
         (If cond l r) ->
-            If (replaceExpr new old cond) (replaceExpr new old l) (replaceExpr new old r)
-        (Unary op value) -> Unary op (replaceExpr new old value)
-        (Binary op l r) -> Binary op (replaceExpr new old l) (replaceExpr new old r)
-        (Agg op inner) -> Agg op (replaceExpr new old inner)
-        (Over keys inner) -> Over keys (replaceExpr new old inner)
+            If (replaceExpr old new cond) (replaceExpr old new l) (replaceExpr old new r)
+        (Unary op value) -> Unary op (replaceExpr old new value)
+        (Binary op l r) -> Binary op (replaceExpr old new l) (replaceExpr old new r)
+        (Agg op inner) -> Agg op (replaceExpr old new inner)
+        (Over keys inner) -> Over keys (replaceExpr old new inner)
 
 {- | Simultaneously substitute 'Col' references from a name→expression map in a
 single parallel pass, so a swap like @{a ↦ col b, b ↦ col a}@ works. Raw-text
@@ -441,8 +436,9 @@
 prettyPrint = prettyPrintWidth P.defaultWidth
 
 {- | Render an expression as readable, width-aware pseudo-code: long binary chains
-wrap onto aligned continuation lines, @if@/@then@/@else@ break onto their own lines
-(nested @else if@ form a flat ladder), and sub-exprs are parenthesized by precedence.
+wrap onto aligned continuation lines, conditionals are laid out like Python
+statements (@if cond@ / @else if cond@ / @else@ with 4-space indented branches, so
+nested conditionals nest visually), and sub-exprs are parenthesized by precedence.
 -}
 prettyPrintWidth :: Int -> Expr a -> String
 prettyPrintWidth width = P.render width . toDoc 0
@@ -505,8 +501,7 @@
     renderIf prec (If c t e) =
         let blk =
                 P.text "if" P.<+> P.nest 3 (P.group (toDoc 0 c))
-                    <> P.hardline
-                    <> P.text "then" P.<+> toDoc 0 t
+                    <> P.nest indentWidth (P.hardline <> toDoc 0 t)
                     <> P.hardline
                     <> renderElse e
          in if prec > 0 then P.parens (P.nest 2 blk) else blk
@@ -514,9 +509,11 @@
 
     renderElse :: Expr x -> P.Doc
     renderElse (If c t e) =
-        P.text "else if" P.<+> P.nest 8 (P.group (toDoc 0 c))
-            <> P.hardline
-            <> P.text "then" P.<+> toDoc 0 t
+        P.text "else if" P.<+> P.nest 5 (P.group (toDoc 0 c))
+            <> P.nest indentWidth (P.hardline <> toDoc 0 t)
             <> P.hardline
             <> renderElse e
-    renderElse other = P.text "else" P.<+> toDoc 0 other
+    renderElse other =
+        P.text "else" <> P.nest indentWidth (P.hardline <> toDoc 0 other)
+    indentWidth :: Int
+    indentWidth = 4
diff --git a/src-internal/DataFrame/Internal/Interpreter.hs b/src-internal/DataFrame/Internal/Interpreter.hs
--- a/src-internal/DataFrame/Internal/Interpreter.hs
+++ b/src-internal/DataFrame/Internal/Interpreter.hs
@@ -49,56 +49,56 @@
 -- Specializations for common aggregation types to avoid dictionary overhead.
 -- foldLinearGroups: mean accumulator
 {-# SPECIALIZE foldLinearGroups ::
-    (MeanAcc -> Double -> MeanAcc) ->
-    MeanAcc ->
+    ((Double, Int) -> Double -> (Double, Int)) ->
+    (Double, Int) ->
     Column ->
     VU.Vector Int ->
     Int ->
     Either DataFrameException Column
     #-}
 {-# SPECIALIZE foldLinearGroups ::
-    (MeanAcc -> Float -> MeanAcc) ->
-    MeanAcc ->
+    ((Double, Int) -> Float -> (Double, Int)) ->
+    (Double, Int) ->
     Column ->
     VU.Vector Int ->
     Int ->
     Either DataFrameException Column
     #-}
 {-# SPECIALIZE foldLinearGroups ::
-    (MeanAcc -> Int -> MeanAcc) ->
-    MeanAcc ->
+    ((Double, Int) -> Int -> (Double, Int)) ->
+    (Double, Int) ->
     Column ->
     VU.Vector Int ->
     Int ->
     Either DataFrameException Column
     #-}
 {-# SPECIALIZE foldLinearGroups ::
-    (MeanAcc -> Int8 -> MeanAcc) ->
-    MeanAcc ->
+    ((Double, Int) -> Int8 -> (Double, Int)) ->
+    (Double, Int) ->
     Column ->
     VU.Vector Int ->
     Int ->
     Either DataFrameException Column
     #-}
 {-# SPECIALIZE foldLinearGroups ::
-    (MeanAcc -> Int16 -> MeanAcc) ->
-    MeanAcc ->
+    ((Double, Int) -> Int16 -> (Double, Int)) ->
+    (Double, Int) ->
     Column ->
     VU.Vector Int ->
     Int ->
     Either DataFrameException Column
     #-}
 {-# SPECIALIZE foldLinearGroups ::
-    (MeanAcc -> Int32 -> MeanAcc) ->
-    MeanAcc ->
+    ((Double, Int) -> Int32 -> (Double, Int)) ->
+    (Double, Int) ->
     Column ->
     VU.Vector Int ->
     Int ->
     Either DataFrameException Column
     #-}
 {-# SPECIALIZE foldLinearGroups ::
-    (MeanAcc -> Int64 -> MeanAcc) ->
-    MeanAcc ->
+    ((Double, Int) -> Int64 -> (Double, Int)) ->
+    (Double, Int) ->
     Column ->
     VU.Vector Int ->
     Int ->
@@ -213,7 +213,7 @@
 
 -- mapColumn: finalize
 {-# SPECIALIZE mapColumn ::
-    (MeanAcc -> Double) -> Column -> Either DataFrameException Column
+    ((Double, Int) -> Double) -> Column -> Either DataFrameException Column
     #-}
 {-# SPECIALIZE mapColumn ::
     (Double -> Double) -> Column -> Either DataFrameException Column
