@@ -680,6 +680,32 @@ def expected_data(self) -> pd.DataFrame:
680680 def input_data (self , expected_data : pd .DataFrame ) -> pd .DataFrame :
681681 return expected_data .copy ()
682682
683+ def _canonical_sort_mixed_safe (self , df : pd .DataFrame ) -> pd .DataFrame :
684+ """Computes a sort index with categorical and object dtype special
685+ case handling. Then uses the index to sort the original df."""
686+ sorted_cols = df .columns .sort_values ().to_list ()
687+ sort_df = df .copy ()
688+
689+ for col in sorted_cols :
690+ series = sort_df [col ]
691+
692+ if isinstance (series .dtype , pd .CategoricalDtype ):
693+ # Avoid unordered categorical sort errors.
694+ sort_df [col ] = series .astype ("string" )
695+ continue
696+
697+ if pd .api .types .is_object_dtype (series ):
698+ try :
699+ series .sort_values ()
700+ except TypeError :
701+ # Normalize only non-orderable mixed object columns.
702+ sort_df [col ] = series .map (
703+ lambda v : "" if pd .isna (v ) else f"{ type (v ).__name__ } :{ v } "
704+ )
705+
706+ order = sort_df .sort_values (by = sorted_cols ).index
707+ return df .loc [order ].reset_index (drop = True )
708+
683709 def test_iamc_data_input (
684710 self ,
685711 run : ixmp4 .Run ,
@@ -691,8 +717,8 @@ def test_iamc_data_input(
691717
692718 ret = run .iamc .tabulate ()
693719 pdt .assert_frame_equal (
694- self .canonical_sort (expected_data ),
695- self .canonical_sort (ret ),
720+ self ._canonical_sort_mixed_safe (expected_data ),
721+ self ._canonical_sort_mixed_safe (ret ),
696722 check_like = True ,
697723 )
698724
@@ -702,10 +728,26 @@ def test_iamc_data_input(
702728 assert run .iamc .tabulate ().empty
703729
704730
705- class TestAnnualIamcInputData (IamcDataAnnual , IamcDataInputTest ):
706- @pytest .fixture
707- def expected_data (self , test_data_add : pd .DataFrame ) -> pd .DataFrame :
708- return test_data_add .copy ()
731+ class TestAnnualIamcInputData (IamcDataInputTest ):
732+ @pytest .fixture (scope = "class" )
733+ def expected_data (
734+ self ,
735+ regions : list [ixmp4 .Region ],
736+ units : list [ixmp4 .Unit ],
737+ ) -> pd .DataFrame :
738+ return pd .DataFrame (
739+ [
740+ ["Region 1" , "Unit 1" , "Variable 1" , 2000 , 1.1 ],
741+ ["Region 1" , "Unit 1" , "Variable 1" , 2010 , 1.3 ],
742+ ["Region 1" , "Unit 2" , "Variable 2" , 2020 , 1.5 ],
743+ ["Region 1" , "Unit 2" , "Variable 2" , 2030 , 1.7 ],
744+ ["Region 2" , "Unit 1" , "Variable 1" , 2000 , 2.1 ],
745+ ["Region 2" , "Unit 1" , "Variable 1" , 2010 , 2.3 ],
746+ ["Region 2" , "Unit 2" , "Variable 2" , 2020 , 2.5 ],
747+ ["Region 2" , "Unit 2" , "Variable 2" , 2030 , 2.7 ],
748+ ],
749+ columns = ["region" , "unit" , "variable" , "year" , "value" ],
750+ ).astype ({"year" : "Int64" })
709751
710752 @pytest .fixture
711753 def input_data (self , expected_data : pd .DataFrame ) -> pd .DataFrame :
@@ -722,15 +764,12 @@ class TestCategoricalIamcInputData(IamcDataInputTest):
722764 @pytest .fixture
723765 def expected_data (self ) -> pd .DataFrame :
724766 return pd .DataFrame (
725- {
726- "region" : ["Region 1" , "Region 2" ],
727- "variable" : ["Variable 1" , "Variable 2" ],
728- "unit" : ["Unit 1" , "Unit 2" ],
729- "year" : pd .Series ([2000 , 2010 ], dtype = "Int64" ),
730- "subannual" : ["Summer" , "Winter" ],
731- "value" : [1.1 , 2.3 ],
732- }
733- )
767+ [
768+ ["Region 1" , "Variable 1" , "Unit 1" , 2000 , "Summer" , 1.1 ],
769+ ["Region 2" , "Variable 2" , "Unit 2" , 2010 , "Winter" , 2.3 ],
770+ ],
771+ columns = ["region" , "variable" , "unit" , "year" , "subannual" , "value" ],
772+ ).astype ({"year" : "Int64" })
734773
735774 @pytest .fixture
736775 def input_data (self , expected_data : pd .DataFrame ) -> pd .DataFrame :
@@ -748,33 +787,95 @@ class TestDatetimeIamcInputData(IamcDataInputTest):
748787 @pytest .fixture
749788 def expected_data (self ) -> pd .DataFrame :
750789 return pd .DataFrame (
751- {
752- "region" : ["Region 1" , "Region 2" ],
753- "variable" : ["Variable 1" , "Variable 2" ],
754- "unit" : ["Unit 1" , "Unit 2" ],
755- "time" : pd .to_datetime (["2000-01-01 00:00:00" , "2010-06-01 12:34:56" ]),
756- "value" : [1.1 , 2.3 ],
757- }
790+ [
791+ [
792+ "Region 1" ,
793+ "Variable 1" ,
794+ "Unit 1" ,
795+ pd .Timestamp ("2000-01-01 00:00:00" ),
796+ 1.1 ,
797+ ],
798+ [
799+ "Region 2" ,
800+ "Variable 2" ,
801+ "Unit 2" ,
802+ pd .Timestamp ("2010-06-01 12:34:56" ),
803+ 2.3 ,
804+ ],
805+ ],
806+ columns = ["region" , "variable" , "unit" , "time" , "value" ],
807+ )
808+
809+
810+ class TestDatetimeAsStringIamcInputData (TestDatetimeIamcInputData ):
811+ @pytest .fixture
812+ def input_data (self , expected_data : pd .DataFrame ) -> pd .DataFrame :
813+ input_df = expected_data .copy ()
814+ input_df ["time" ] = (
815+ input_df ["time" ].dt .strftime ("%Y-%m-%d %H:%M:%S" ).astype ("string" )
816+ )
817+ return input_df
818+
819+
820+ class TestSingleRowDatetimeIamcInputData (IamcDataInputTest ):
821+ @pytest .fixture
822+ def expected_data (self ) -> pd .DataFrame :
823+ return pd .DataFrame (
824+ [
825+ [
826+ "Region 1" ,
827+ "Variable 1" ,
828+ "Unit 1" ,
829+ pd .Timestamp ("2000-01-01 00:00:00" ),
830+ 1.1 ,
831+ ],
832+ ],
833+ columns = ["region" , "variable" , "unit" , "time" , "value" ],
758834 )
759835
760836 @pytest .fixture
761837 def input_data (self , expected_data : pd .DataFrame ) -> pd .DataFrame :
762838 input_df = expected_data .copy ()
763- input_df ["region" ] = input_df ["region" ].astype ("category" )
764- input_df ["unit" ] = input_df ["unit" ].astype ("category" )
765- input_df ["variable" ] = input_df ["variable" ].astype ("category" )
766839 input_df ["time" ] = (
767840 input_df ["time" ].dt .strftime ("%Y-%m-%d %H:%M:%S" ).astype ("string" )
768841 )
769- input_df ["value" ] = input_df ["value" ].astype ("float32" )
770842 return input_df
771843
772844
773- class TestObjectStringsIamcInputData ( IamcDataAnnual , IamcDataInputTest ):
845+ class TestMixedIamcInputData ( IamcDataInputTest ):
774846 @pytest .fixture
775- def expected_data (self , test_data_add : pd .DataFrame ) -> pd .DataFrame :
776- return test_data_add .copy ()
847+ def expected_data (self ) -> pd .DataFrame :
848+ return pd .DataFrame (
849+ [
850+ # ANNUAL
851+ ["Region 1" , "Variable 1" , "Unit 1" , 2000 , None , 0.1 ],
852+ ["Region 2" , "Variable 2" , "Unit 2" , 2010 , None , 0.23 ],
853+ # CATEGORICAL
854+ ["Region 1" , "Variable 1" , "Unit 1" , 2000 , "Summer" , 1.1 ],
855+ ["Region 2" , "Variable 2" , "Unit 2" , 2010 , "Winter" , 2.3 ],
856+ # DATETIME
857+ [
858+ "Region 1" ,
859+ "Variable 1" ,
860+ "Unit 1" ,
861+ pd .Timestamp ("2000-01-01 00:00:00" ),
862+ None ,
863+ 101.0 ,
864+ ],
865+ [
866+ "Region 2" ,
867+ "Variable 2" ,
868+ "Unit 2" ,
869+ pd .Timestamp ("2010-06-01 12:34:56" ),
870+ None ,
871+ 3.14 ,
872+ ],
873+ ],
874+ columns = ["region" , "variable" , "unit" , "time" , "subannual" , "value" ],
875+ )
876+
777877
878+ class TestObjectStringsIamcInputData (TestAnnualIamcInputData ):
778879 @pytest .fixture
779880 def input_data (self , expected_data : pd .DataFrame ) -> pd .DataFrame :
780881 input_df = expected_data .copy ()
0 commit comments