diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 10cf433a8b..4c0027ff1c 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:8ff1efe878e18bd82a0fb7b70bb86f77e7ab6901fed394440b6135db0ba8d84a -# created: 2025-01-09T12:01:16.422459506Z + digest: sha256:04c35dc5f49f0f503a306397d6d043685f8d2bb822ab515818c4208d7fb2db3a +# created: 2025-01-16T15:24:11.364245182Z diff --git a/.kokoro/docker/docs/requirements.in b/.kokoro/docker/docs/requirements.in index 816817c672..586bd07037 100644 --- a/.kokoro/docker/docs/requirements.in +++ b/.kokoro/docker/docs/requirements.in @@ -1 +1,2 @@ nox +gcp-docuploader diff --git a/.kokoro/docker/docs/requirements.txt b/.kokoro/docker/docs/requirements.txt index f99a5c4aac..a9360a25b7 100644 --- a/.kokoro/docker/docs/requirements.txt +++ b/.kokoro/docker/docs/requirements.txt @@ -2,16 +2,124 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile --allow-unsafe --generate-hashes synthtool/gcp/templates/python_library/.kokoro/docker/docs/requirements.in +# pip-compile --allow-unsafe --generate-hashes requirements.in # -argcomplete==3.5.2 \ - --hash=sha256:036d020d79048a5d525bc63880d7a4b8d1668566b8a76daf1144c0bbe0f63472 \ - --hash=sha256:23146ed7ac4403b70bd6026402468942ceba34a6732255b9edf5b7354f68a6bb +argcomplete==3.5.3 \ + --hash=sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61 \ + --hash=sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392 # via nox +cachetools==5.5.0 \ + --hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \ + --hash=sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a + # via google-auth +certifi==2024.12.14 \ + --hash=sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56 \ + --hash=sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db + # via requests +charset-normalizer==3.4.1 \ + --hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \ + --hash=sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa \ + --hash=sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a \ + --hash=sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294 \ + --hash=sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b \ + --hash=sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd \ + --hash=sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601 \ + --hash=sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd \ + --hash=sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4 \ + --hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \ + --hash=sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2 \ + --hash=sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313 \ + --hash=sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd \ + --hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \ + --hash=sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8 \ + --hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1 \ + --hash=sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2 \ + --hash=sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496 \ + --hash=sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d \ + --hash=sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b \ + --hash=sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e \ + --hash=sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a \ + --hash=sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4 \ + --hash=sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca \ + --hash=sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78 \ + --hash=sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408 \ + --hash=sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5 \ + --hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \ + --hash=sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f \ + --hash=sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a \ + --hash=sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765 \ + --hash=sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6 \ + --hash=sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146 \ + --hash=sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6 \ + --hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \ + --hash=sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd \ + --hash=sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c \ + --hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \ + --hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \ + --hash=sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176 \ + --hash=sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770 \ + --hash=sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824 \ + --hash=sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f \ + --hash=sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf \ + --hash=sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487 \ + --hash=sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d \ + --hash=sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd \ + --hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \ + --hash=sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534 \ + --hash=sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f \ + --hash=sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b \ + --hash=sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9 \ + --hash=sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd \ + --hash=sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125 \ + --hash=sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9 \ + --hash=sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de \ + --hash=sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 \ + --hash=sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d \ + --hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \ + --hash=sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f \ + --hash=sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda \ + --hash=sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7 \ + --hash=sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a \ + --hash=sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971 \ + --hash=sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8 \ + --hash=sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41 \ + --hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \ + --hash=sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f \ + --hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \ + --hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \ + --hash=sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886 \ + --hash=sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77 \ + --hash=sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76 \ + --hash=sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247 \ + --hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \ + --hash=sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb \ + --hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \ + --hash=sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e \ + --hash=sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6 \ + --hash=sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037 \ + --hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \ + --hash=sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e \ + --hash=sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807 \ + --hash=sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407 \ + --hash=sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c \ + --hash=sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12 \ + --hash=sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3 \ + --hash=sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089 \ + --hash=sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd \ + --hash=sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e \ + --hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \ + --hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616 + # via requests +click==8.1.8 \ + --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \ + --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a + # via gcp-docuploader colorlog==6.9.0 \ --hash=sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff \ --hash=sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2 - # via nox + # via + # gcp-docuploader + # nox distlib==0.3.9 \ --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 @@ -20,10 +128,78 @@ filelock==3.16.1 \ --hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \ --hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435 # via virtualenv +gcp-docuploader==0.6.5 \ + --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ + --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea + # via -r requirements.in +google-api-core==2.24.0 \ + --hash=sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9 \ + --hash=sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf + # via + # google-cloud-core + # google-cloud-storage +google-auth==2.37.0 \ + --hash=sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00 \ + --hash=sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0 + # via + # google-api-core + # google-cloud-core + # google-cloud-storage +google-cloud-core==2.4.1 \ + --hash=sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073 \ + --hash=sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61 + # via google-cloud-storage +google-cloud-storage==2.19.0 \ + --hash=sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba \ + --hash=sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2 + # via gcp-docuploader +google-crc32c==1.6.0 \ + --hash=sha256:05e2d8c9a2f853ff116db9706b4a27350587f341eda835f46db3c0a8c8ce2f24 \ + --hash=sha256:18e311c64008f1f1379158158bb3f0c8d72635b9eb4f9545f8cf990c5668e59d \ + --hash=sha256:236c87a46cdf06384f614e9092b82c05f81bd34b80248021f729396a78e55d7e \ + --hash=sha256:35834855408429cecf495cac67ccbab802de269e948e27478b1e47dfb6465e57 \ + --hash=sha256:386122eeaaa76951a8196310432c5b0ef3b53590ef4c317ec7588ec554fec5d2 \ + --hash=sha256:40b05ab32a5067525670880eb5d169529089a26fe35dce8891127aeddc1950e8 \ + --hash=sha256:48abd62ca76a2cbe034542ed1b6aee851b6f28aaca4e6551b5599b6f3ef175cc \ + --hash=sha256:50cf2a96da226dcbff8671233ecf37bf6e95de98b2a2ebadbfdf455e6d05df42 \ + --hash=sha256:51c4f54dd8c6dfeb58d1df5e4f7f97df8abf17a36626a217f169893d1d7f3e9f \ + --hash=sha256:5bcc90b34df28a4b38653c36bb5ada35671ad105c99cfe915fb5bed7ad6924aa \ + --hash=sha256:62f6d4a29fea082ac4a3c9be5e415218255cf11684ac6ef5488eea0c9132689b \ + --hash=sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc \ + --hash=sha256:7aec8e88a3583515f9e0957fe4f5f6d8d4997e36d0f61624e70469771584c760 \ + --hash=sha256:91ca8145b060679ec9176e6de4f89b07363d6805bd4760631ef254905503598d \ + --hash=sha256:a184243544811e4a50d345838a883733461e67578959ac59964e43cca2c791e7 \ + --hash=sha256:a9e4b426c3702f3cd23b933436487eb34e01e00327fac20c9aebb68ccf34117d \ + --hash=sha256:bb0966e1c50d0ef5bc743312cc730b533491d60585a9a08f897274e57c3f70e0 \ + --hash=sha256:bb8b3c75bd157010459b15222c3fd30577042a7060e29d42dabce449c087f2b3 \ + --hash=sha256:bd5e7d2445d1a958c266bfa5d04c39932dc54093fa391736dbfdb0f1929c1fb3 \ + --hash=sha256:c87d98c7c4a69066fd31701c4e10d178a648c2cac3452e62c6b24dc51f9fcc00 \ + --hash=sha256:d2952396dc604544ea7476b33fe87faedc24d666fb0c2d5ac971a2b9576ab871 \ + --hash=sha256:d8797406499f28b5ef791f339594b0b5fdedf54e203b5066675c406ba69d705c \ + --hash=sha256:d9e9913f7bd69e093b81da4535ce27af842e7bf371cde42d1ae9e9bd382dc0e9 \ + --hash=sha256:e2806553238cd076f0a55bddab37a532b53580e699ed8e5606d0de1f856b5205 \ + --hash=sha256:ebab974b1687509e5c973b5c4b8b146683e101e102e17a86bd196ecaa4d099fc \ + --hash=sha256:ed767bf4ba90104c1216b68111613f0d5926fb3780660ea1198fc469af410e9d \ + --hash=sha256:f7a1fc29803712f80879b0806cb83ab24ce62fc8daf0569f2204a0cfd7f68ed4 + # via + # google-cloud-storage + # google-resumable-media +google-resumable-media==2.7.2 \ + --hash=sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa \ + --hash=sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0 + # via google-cloud-storage +googleapis-common-protos==1.66.0 \ + --hash=sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c \ + --hash=sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed + # via google-api-core +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via requests nox==2024.10.9 \ --hash=sha256:1d36f309a0a2a853e9bccb76bbef6bb118ba92fa92674d15604ca99adeb29eab \ --hash=sha256:7aa9dc8d1c27e9f45ab046ffd1c3b2c4f7c91755304769df231308849ebded95 - # via -r synthtool/gcp/templates/python_library/.kokoro/docker/docs/requirements.in + # via -r requirements.in packaging==24.2 \ --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f @@ -32,6 +208,51 @@ platformdirs==4.3.6 \ --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb # via virtualenv +proto-plus==1.25.0 \ + --hash=sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961 \ + --hash=sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91 + # via google-api-core +protobuf==5.29.3 \ + --hash=sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f \ + --hash=sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7 \ + --hash=sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888 \ + --hash=sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620 \ + --hash=sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da \ + --hash=sha256:84a57163a0ccef3f96e4b6a20516cedcf5bb3a95a657131c5c3ac62200d23252 \ + --hash=sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a \ + --hash=sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e \ + --hash=sha256:b89c115d877892a512f79a8114564fb435943b59067615894c3b13cd3e1fa107 \ + --hash=sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f \ + --hash=sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84 + # via + # gcp-docuploader + # google-api-core + # googleapis-common-protos + # proto-plus +pyasn1==0.6.1 \ + --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ + --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.4.1 \ + --hash=sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd \ + --hash=sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c + # via google-auth +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 + # via + # google-api-core + # google-cloud-storage +rsa==4.9 \ + --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \ + --hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21 + # via google-auth +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via gcp-docuploader tomli==2.2.1 \ --hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \ --hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \ @@ -66,7 +287,11 @@ tomli==2.2.1 \ --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \ --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7 # via nox -virtualenv==20.28.0 \ - --hash=sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0 \ - --hash=sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa +urllib3==2.3.0 \ + --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ + --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d + # via requests +virtualenv==20.28.1 \ + --hash=sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb \ + --hash=sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329 # via nox diff --git a/.kokoro/publish-docs.sh b/.kokoro/publish-docs.sh index 297b14ac90..2d5ba47549 100755 --- a/.kokoro/publish-docs.sh +++ b/.kokoro/publish-docs.sh @@ -20,10 +20,6 @@ export PYTHONUNBUFFERED=1 export PATH="${HOME}/.local/bin:${PATH}" -# Install nox -python3.10 -m pip install --require-hashes -r .kokoro/requirements.txt -python3.10 -m nox --version - # build docs nox -s docs diff --git a/CHANGELOG.md b/CHANGELOG.md index b4bec86e9e..d8befd372d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.33.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.32.0...v1.33.0) (2025-01-22) + + +### Features + +* Add `bigframes.bigquery.sql_scalar()` to apply SQL syntax on Series objects ([#1293](https://github.com/googleapis/python-bigquery-dataframes/issues/1293)) ([aa2f73a](https://github.com/googleapis/python-bigquery-dataframes/commit/aa2f73ad86e42c37d85ac867a3702eb6f2724b11)) +* Add unix_seconds, unix_millis and unix_micros for timestamp series. ([#1297](https://github.com/googleapis/python-bigquery-dataframes/issues/1297)) ([e4b0c8d](https://github.com/googleapis/python-bigquery-dataframes/commit/e4b0c8dd9edda48e07c433b99f44db82e1ea2054)) +* DataFrame.join supports Series other ([#1303](https://github.com/googleapis/python-bigquery-dataframes/issues/1303)) ([ee37a0a](https://github.com/googleapis/python-bigquery-dataframes/commit/ee37a0ab84e9415046e0e15955c14a1965b3a904)) +* Support array output in `remote_function` ([#1057](https://github.com/googleapis/python-bigquery-dataframes/issues/1057)) ([bdee173](https://github.com/googleapis/python-bigquery-dataframes/commit/bdee1734809589e5a7a3c23ee9cd2f967adf346f)) + + +### Bug Fixes + +* Dataframe sort_values Series input keyerror. ([#1285](https://github.com/googleapis/python-bigquery-dataframes/issues/1285)) ([5a2731b](https://github.com/googleapis/python-bigquery-dataframes/commit/5a2731bda8b2e9ea54bf582f823acdb6153dbb8f)) +* Fix read_gbq_function issue in dataframe apply method ([#1174](https://github.com/googleapis/python-bigquery-dataframes/issues/1174)) ([0318764](https://github.com/googleapis/python-bigquery-dataframes/commit/0318764030f6753a4e925c62612aabbb8e192fdf)) +* Series sort_index and sort_values now raises when axis!=0 ([#1294](https://github.com/googleapis/python-bigquery-dataframes/issues/1294)) ([94bc2f2](https://github.com/googleapis/python-bigquery-dataframes/commit/94bc2f2dc3514fffeac625592ec4b28c32957723)) + + +### Documentation + +* Add snippet to forecast future time series in the Forecast a single time series with a univariate model tutorial ([#1271](https://github.com/googleapis/python-bigquery-dataframes/issues/1271)) ([a687050](https://github.com/googleapis/python-bigquery-dataframes/commit/a687050b2a92bed1af9cb86a812b62f9a69cf959)) +* Update `bigframes.pandas.Series` docs ([#1273](https://github.com/googleapis/python-bigquery-dataframes/issues/1273)) ([0cac64f](https://github.com/googleapis/python-bigquery-dataframes/commit/0cac64f5ba3f3c9e8495fc5acb09d81c39d36de0)) + ## [1.32.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.31.0...v1.32.0) (2025-01-13) diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py index ff52ae8d36..21e61bc4b1 100644 --- a/bigframes/bigquery/__init__.py +++ b/bigframes/bigquery/__init__.py @@ -22,6 +22,11 @@ array_length, array_to_string, ) +from bigframes.bigquery._operations.datetime import ( + unix_micros, + unix_millis, + unix_seconds, +) from bigframes.bigquery._operations.json import ( json_extract, json_extract_array, @@ -30,6 +35,7 @@ parse_json, ) from bigframes.bigquery._operations.search import create_vector_index, vector_search +from bigframes.bigquery._operations.sql import sql_scalar from bigframes.bigquery._operations.struct import struct __all__ = [ @@ -48,6 +54,12 @@ # search ops "create_vector_index", "vector_search", + # sql ops + "sql_scalar", # struct ops "struct", + # datetime ops + "unix_micros", + "unix_millis", + "unix_seconds", ] diff --git a/bigframes/bigquery/_operations/datetime.py b/bigframes/bigquery/_operations/datetime.py new file mode 100644 index 0000000000..f8767336dd --- /dev/null +++ b/bigframes/bigquery/_operations/datetime.py @@ -0,0 +1,97 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from bigframes import operations as ops +from bigframes import series + + +def unix_seconds(input: series.Series) -> series.Series: + """Converts a timestmap series to unix epoch seconds + + **Examples:** + + >>> import pandas as pd + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([pd.Timestamp("1970-01-02", tz="UTC"), pd.Timestamp("1970-01-03", tz="UTC")]) + >>> bbq.unix_seconds(s) + 0 86400 + 1 172800 + dtype: Int64 + + Args: + input (bigframes.pandas.Series): + A timestamp series. + + Returns: + bigframes.pandas.Series: A new series of unix epoch in seconds. + + """ + return input._apply_unary_op(ops.UnixSeconds()) + + +def unix_millis(input: series.Series) -> series.Series: + """Converts a timestmap series to unix epoch milliseconds + + **Examples:** + + >>> import pandas as pd + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([pd.Timestamp("1970-01-02", tz="UTC"), pd.Timestamp("1970-01-03", tz="UTC")]) + >>> bbq.unix_millis(s) + 0 86400000 + 1 172800000 + dtype: Int64 + + Args: + input (bigframes.pandas.Series): + A timestamp series. + + Returns: + bigframes.pandas.Series: A new series of unix epoch in milliseconds. + + """ + return input._apply_unary_op(ops.UnixMillis()) + + +def unix_micros(input: series.Series) -> series.Series: + """Converts a timestmap series to unix epoch microseconds + + **Examples:** + + >>> import pandas as pd + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([pd.Timestamp("1970-01-02", tz="UTC"), pd.Timestamp("1970-01-03", tz="UTC")]) + >>> bbq.unix_micros(s) + 0 86400000000 + 1 172800000000 + dtype: Int64 + + Args: + input (bigframes.pandas.Series): + A timestamp series. + + Returns: + bigframes.pandas.Series: A new series of unix epoch in microseconds. + + """ + return input._apply_unary_op(ops.UnixMicros()) diff --git a/bigframes/bigquery/_operations/search.py b/bigframes/bigquery/_operations/search.py index 35633fd4ce..9a1e4b5ac9 100644 --- a/bigframes/bigquery/_operations/search.py +++ b/bigframes/bigquery/_operations/search.py @@ -122,12 +122,12 @@ def vector_search( ... base_table="bigframes-dev.bigframes_tests_sys.base_table", ... column_to_search="my_embedding", ... query=search_query, - ... top_k=2) + ... top_k=2).sort_values("id") query_id embedding id my_embedding distance - 1 cat [3. 5.2] 5 [5. 5.4] 2.009975 0 dog [1. 2.] 1 [1. 2.] 0.0 - 0 dog [1. 2.] 4 [1. 3.2] 1.2 1 cat [3. 5.2] 2 [2. 4.] 1.56205 + 0 dog [1. 2.] 4 [1. 3.2] 1.2 + 1 cat [3. 5.2] 5 [5. 5.4] 2.009975 [4 rows x 5 columns] @@ -141,12 +141,12 @@ def vector_search( ... column_to_search="my_embedding", ... query=search_query, ... top_k=2, - ... use_brute_force=True) + ... use_brute_force=True).sort_values("id") embedding id my_embedding distance dog [1. 2.] 1 [1. 2.] 0.0 - cat [3. 5.2] 5 [5. 5.4] 2.009975 - dog [1. 2.] 4 [1. 3.2] 1.2 cat [3. 5.2] 2 [2. 4.] 1.56205 + dog [1. 2.] 4 [1. 3.2] 1.2 + cat [3. 5.2] 5 [5. 5.4] 2.009975 [4 rows x 4 columns] diff --git a/bigframes/bigquery/_operations/sql.py b/bigframes/bigquery/_operations/sql.py new file mode 100644 index 0000000000..7ccf63fcda --- /dev/null +++ b/bigframes/bigquery/_operations/sql.py @@ -0,0 +1,94 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""SQL escape hatch features.""" + +from __future__ import annotations + +from typing import Sequence + +import google.cloud.bigquery + +import bigframes.core.sql +import bigframes.dataframe +import bigframes.dtypes +import bigframes.operations +import bigframes.series + + +def sql_scalar( + sql_template: str, + columns: Sequence[bigframes.series.Series], +) -> bigframes.series.Series: + """Create a Series from a SQL template. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> import pandas as pd + >>> import pyarrow as pa + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(["1.5", "2.5", "3.5"]) + >>> s = s.astype(pd.ArrowDtype(pa.decimal128(38, 9))) + >>> bbq.sql_scalar("ROUND({0}, 0, 'ROUND_HALF_EVEN')", [s]) + 0 2.000000000 + 1 2.000000000 + 2 4.000000000 + dtype: decimal128(38, 9)[pyarrow] + + Args: + sql_template (str): + A SQL format string with Python-style {0} placeholders for each of + the Series objects in ``columns``. + columns (Sequence[bigframes.pandas.Series]): + Series objects representing the column inputs to the + ``sql_template``. Must contain at least one Series. + + Returns: + bigframes.pandas.Series: + A Series with the SQL applied. + + Raises: + ValueError: If ``columns`` is empty. + """ + if len(columns) == 0: + raise ValueError("Must provide at least one column in columns") + + # To integrate this into our expression trees, we need to get the output + # type, so we do some manual compilation and a dry run query to get that. + # Another benefit of this is that if there is a syntax error in the SQL + # template, then this will fail with an error earlier in the process, + # aiding users in debugging. + base_series = columns[0] + literals = [ + bigframes.dtypes.bigframes_dtype_to_literal(column.dtype) for column in columns + ] + literals_sql = [bigframes.core.sql.simple_literal(literal) for literal in literals] + + # Use the executor directly, because we want the original column IDs, not + # the user-friendly column names that block.to_sql_query() would produce. + select_sql = sql_template.format(*literals_sql) + dry_run_sql = f"SELECT {select_sql}" + bqclient = base_series._session.bqclient + job = bqclient.query( + dry_run_sql, job_config=google.cloud.bigquery.QueryJobConfig(dry_run=True) + ) + _, output_type = bigframes.dtypes.convert_schema_field(job.schema[0]) + + op = bigframes.operations.SqlScalarOp( + _output_type=output_type, sql_template=sql_template + ) + return base_series._apply_nary_op(op, columns[1:]) diff --git a/bigframes/blob/_functions.py b/bigframes/blob/_functions.py index 4b3841252c..397a37ee92 100644 --- a/bigframes/blob/_functions.py +++ b/bigframes/blob/_functions.py @@ -18,7 +18,7 @@ import google.cloud.bigquery as bigquery -import bigframes +import bigframes.session import bigframes.session._io.bigquery as bf_io_bigquery _PYTHON_TO_BQ_TYPES = {int: "INT64", float: "FLOAT64", str: "STRING", bytes: "BYTES"} @@ -37,7 +37,7 @@ class TransformFunction: """Simple transform function class to deal with Python UDF.""" def __init__( - self, func_def: FunctionDef, session: bigframes.Session, connection: str + self, func_def: FunctionDef, session: bigframes.session.Session, connection: str ): self._func = func_def.func self._requirements = func_def.requirements diff --git a/bigframes/clients.py b/bigframes/clients.py index 8a2dbfed6c..c6e1d47909 100644 --- a/bigframes/clients.py +++ b/bigframes/clients.py @@ -21,6 +21,7 @@ from typing import cast, Optional import google.api_core.exceptions +import google.api_core.retry from google.cloud import bigquery_connection_v1, resourcemanager_v3 from google.iam.v1 import iam_policy_pb2, policy_pb2 diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index a88e365dcd..ee9917f619 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -220,8 +220,14 @@ def filter_by_id(self, predicate_id: str, keep_null: bool = False) -> ArrayValue def filter(self, predicate: ex.Expression): return ArrayValue(nodes.FilterNode(child=self.node, predicate=predicate)) - def order_by(self, by: Sequence[OrderingExpression]) -> ArrayValue: - return ArrayValue(nodes.OrderByNode(child=self.node, by=tuple(by))) + def order_by( + self, by: Sequence[OrderingExpression], is_total_order: bool = False + ) -> ArrayValue: + return ArrayValue( + nodes.OrderByNode( + child=self.node, by=tuple(by), is_total_order=is_total_order + ) + ) def reversed(self) -> ArrayValue: return ArrayValue(nodes.ReversedNode(child=self.node)) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index a7f75e7264..8ef3aa123b 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -43,19 +43,16 @@ def equals(block1: blocks.Block, block2: blocks.Block) -> bool: joined_block, (lmap, rmap) = block1.join(block2, how="outer") - equality_ids = [] + exprs = [] for lcol, rcol in zip(block1.value_columns, block2.value_columns): - lcolmapped = lmap[lcol] - rcolmapped = rmap[rcol] - joined_block, result_id = joined_block.project_expr( + exprs.append( ops.fillna_op.as_expr( - ops.eq_null_match_op.as_expr(lcolmapped, rcolmapped), ex.const(False) + ops.eq_null_match_op.as_expr(lmap[lcol], rmap[rcol]), ex.const(False) ) ) - equality_ids.append(result_id) - joined_block = joined_block.select_columns(equality_ids).with_column_labels( - list(range(len(equality_ids))) + joined_block = joined_block.project_exprs( + exprs, labels=list(range(len(exprs))), drop=True ) stacked_block = joined_block.stack() result = stacked_block.get_stat(stacked_block.value_columns[0], agg_ops.all_op) @@ -132,12 +129,16 @@ def quantile( window_spec=window, ) quantile_cols.append(quantile_col) - block, results = block.aggregate( + block, _ = block.aggregate( grouping_column_ids, - tuple((col, agg_ops.AnyValueOp()) for col in quantile_cols), + tuple( + ex.UnaryAggregation(agg_ops.AnyValueOp(), ex.deref(col)) + for col in quantile_cols + ), + column_labels=pd.Index(labels), dropna=dropna, ) - return block.select_columns(results).with_column_labels(labels) + return block def interpolate(block: blocks.Block, method: str = "linear") -> blocks.Block: @@ -358,7 +359,7 @@ def value_counts( block, dummy = block.create_constant(1) block, agg_ids = block.aggregate( by_column_ids=columns, - aggregations=[(dummy, agg_ops.count_op)], + aggregations=[ex.UnaryAggregation(agg_ops.count_op, ex.deref(dummy))], dropna=dropna, ) count_id = agg_ids[0] @@ -395,12 +396,12 @@ def pct_change(block: blocks.Block, periods: int = 1) -> blocks.Block: block, shift_columns = block.multi_apply_window_op( original_columns, agg_ops.ShiftOp(periods), window_spec=window_spec ) - result_ids = [] + exprs = [] for original_col, shifted_col in zip(original_columns, shift_columns): - block, change_id = block.apply_binary_op(original_col, shifted_col, ops.sub_op) - block, pct_change_id = block.apply_binary_op(change_id, shifted_col, ops.div_op) - result_ids.append(pct_change_id) - return block.select_columns(result_ids).with_column_labels(column_labels) + change_expr = ops.sub_op.as_expr(original_col, shifted_col) + pct_change_expr = ops.div_op.as_expr(change_expr, shifted_col) + exprs.append(pct_change_expr) + return block.project_exprs(exprs, labels=column_labels, drop=True) def rank( @@ -470,16 +471,23 @@ def rank( # Step 3: post processing: mask null values and cast to float if method in ["min", "max", "first", "dense"]: # Pandas rank always produces Float64, so must cast for aggregation types that produce ints - block = block.multi_apply_unary_op( - rownum_col_ids, ops.AsTypeOp(pd.Float64Dtype()) + return ( + block.select_columns(rownum_col_ids) + .multi_apply_unary_op(ops.AsTypeOp(pd.Float64Dtype())) + .with_column_labels(labels) ) if na_option == "keep": # For na_option "keep", null inputs must produce null outputs + exprs = [] for i in range(len(columns)): - block, null_const = block.create_constant(pd.NA, dtype=pd.Float64Dtype()) - block, rownum_col_ids[i] = block.apply_ternary_op( - null_const, nullity_col_ids[i], rownum_col_ids[i], ops.where_op + exprs.append( + ops.where_op.as_expr( + ex.const(pd.NA, dtype=pd.Float64Dtype()), + nullity_col_ids[i], + rownum_col_ids[i], + ) ) + return block.project_exprs(exprs, labels=labels, drop=True) return block.select_columns(rownum_col_ids).with_column_labels(labels) @@ -585,9 +593,18 @@ def skew( # counts, moment3 for each column aggregations = [] for i, col in enumerate(original_columns): - count_agg = (col, agg_ops.count_op) - moment3_agg = (delta3_ids[i], agg_ops.mean_op) - variance_agg = (col, agg_ops.PopVarOp()) + count_agg = ex.UnaryAggregation( + agg_ops.count_op, + ex.deref(col), + ) + moment3_agg = ex.UnaryAggregation( + agg_ops.mean_op, + ex.deref(delta3_ids[i]), + ) + variance_agg = ex.UnaryAggregation( + agg_ops.PopVarOp(), + ex.deref(col), + ) aggregations.extend([count_agg, moment3_agg, variance_agg]) block, agg_ids = block.aggregate( @@ -627,9 +644,9 @@ def kurt( # counts, moment4 for each column aggregations = [] for i, col in enumerate(original_columns): - count_agg = (col, agg_ops.count_op) - moment4_agg = (delta4_ids[i], agg_ops.mean_op) - variance_agg = (col, agg_ops.PopVarOp()) + count_agg = ex.UnaryAggregation(agg_ops.count_op, ex.deref(col)) + moment4_agg = ex.UnaryAggregation(agg_ops.mean_op, ex.deref(delta4_ids[i])) + variance_agg = ex.UnaryAggregation(agg_ops.PopVarOp(), ex.deref(col)) aggregations.extend([count_agg, moment4_agg, variance_agg]) block, agg_ids = block.aggregate( diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 522d1743ff..afc03dbdea 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -897,7 +897,6 @@ def multi_apply_window_op( def multi_apply_unary_op( self, - columns: typing.Sequence[str], op: Union[ops.UnaryOp, ex.Expression], ) -> Block: if isinstance(op, ops.UnaryOp): @@ -911,27 +910,37 @@ def multi_apply_unary_op( block = self - result_ids = [] - for col_id in columns: - label = self.col_id_to_label[col_id] - block, result_id = block.project_expr( - expr.bind_variables({input_varname: ex.deref(col_id)}), - label=label, - ) - block = block.copy_values(result_id, col_id) - result_ids.append(result_id) - block = block.drop_columns(result_ids) + exprs = [ + expr.bind_variables({input_varname: ex.deref(col_id)}) + for col_id in self.value_columns + ] + block = self.project_exprs(exprs, labels=self.column_labels, drop=True) + # Special case, we can preserve transpose cache for full-frame unary ops - if (self._transpose_cache is not None) and set(self.value_columns) == set( - columns - ): - transpose_columns = self._transpose_cache.value_columns - new_transpose_cache = self._transpose_cache.multi_apply_unary_op( - transpose_columns, op - ) + if self._transpose_cache is not None: + new_transpose_cache = self._transpose_cache.multi_apply_unary_op(op) block = block.with_transpose_cache(new_transpose_cache) return block + def project_exprs( + self, + exprs: Sequence[ex.Expression], + labels: Union[Sequence[Label], pd.Index], + drop=False, + ) -> Block: + new_array, _ = self.expr.compute_values(exprs) + if drop: + new_array = new_array.drop_columns(self.value_columns) + + return Block( + new_array, + index_columns=self.index_columns, + column_labels=labels + if drop + else self.column_labels.append(pd.Index(labels)), + index_labels=self._index_labels, + ) + def apply_window_op( self, column: str, @@ -1165,38 +1174,31 @@ def remap_f(x): def aggregate( self, by_column_ids: typing.Sequence[str] = (), - aggregations: typing.Sequence[ - typing.Tuple[ - str, typing.Union[agg_ops.UnaryAggregateOp, agg_ops.NullaryAggregateOp] - ] - ] = (), + aggregations: typing.Sequence[ex.Aggregation] = (), + column_labels: Optional[pd.Index] = None, *, dropna: bool = True, ) -> typing.Tuple[Block, typing.Sequence[str]]: """ - Apply aggregations to the block. Callers responsible for setting index column(s) after. + Apply aggregations to the block. Arguments: by_column_id: column id of the aggregation key, this is preserved through the transform and used as index. aggregations: input_column_id, operation tuples - as_index: if True, grouping keys will be index columns in result, otherwise they will be non-index columns. dropna: whether null keys should be dropped """ + if column_labels is None: + column_labels = pd.Index(range(len(aggregations))) + agg_specs = [ ( - ex.UnaryAggregation(operation, ex.deref(input_id)) - if isinstance(operation, agg_ops.UnaryAggregateOp) - else ex.NullaryAggregation(operation), + aggregation, guid.generate_guid(), ) - for input_id, operation in aggregations + for aggregation in aggregations ] output_col_ids = [agg_spec[1] for agg_spec in agg_specs] result_expr = self.expr.aggregate(agg_specs, by_column_ids, dropna=dropna) - aggregate_labels = self._get_labels_for_columns( - [agg[0] for agg in aggregations] - ) - names: typing.List[Label] = [] if len(by_column_ids) == 0: result_expr, label_id = result_expr.create_constant(0, pd.Int64Dtype()) @@ -1214,7 +1216,7 @@ def aggregate( Block( result_expr, index_columns=index_columns, - column_labels=aggregate_labels, + column_labels=column_labels, index_labels=names, ), output_col_ids, @@ -1552,7 +1554,10 @@ def pivot( column_ids.append(masked_id) block = block.select_columns(column_ids) - aggregations = [(col_id, agg_ops.AnyValueOp()) for col_id in column_ids] + aggregations = [ + ex.UnaryAggregation(agg_ops.AnyValueOp(), ex.deref(col_id)) + for col_id in column_ids + ] result_block, _ = block.aggregate( by_column_ids=self.index_columns, aggregations=aggregations, @@ -2279,18 +2284,15 @@ def _apply_binop( labels: pd.Index, reverse: bool = False, ) -> Block: - block = self - binop_result_ids = [] + exprs = [] for left_input, right_input in inputs: - expr = ( + exprs.append( op.as_expr(right_input, left_input) if reverse else op.as_expr(left_input, right_input) ) - block, result_col_id = block.project_expr(expr) - binop_result_ids.append(result_col_id) - return block.select_columns(binop_result_ids).with_column_labels(labels) + return self.project_exprs(exprs, labels=labels, drop=True) def join( self, diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index f879eb3feb..526826495e 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -278,6 +278,24 @@ def to_sql( sql = ibis_bigquery.Backend().compile(self._to_ibis_expr()) return typing.cast(str, sql) + def with_total_order(self, by: Sequence[OrderingExpression]) -> OrderedIR: + return OrderedIR( + table=self._table, + columns=self._columns, + predicates=self._predicates, + ordering=TotalOrdering( + ordering_value_columns=tuple(by), + total_ordering_columns=frozenset( + map( + ex.DerefOp, + itertools.chain.from_iterable( + col.referenced_columns for col in by + ), + ) + ), + ), + ) + def row_count(self, name: str) -> OrderedIR: original_table = self._to_ibis_expr() ibis_table = original_table.agg( @@ -576,6 +594,13 @@ def __init__( def is_ordered_ir(self) -> bool: return True + @property + def order_non_deterministic(self) -> bool: + # ordering suffix non-determinism is ok, as rand() is used as suffix for auto-generated order keys. + # but must be resolved before or explode, otherwise the engine might pull the rand() evaluation above the join, + # creating inconsistencies + return not all(col.deterministic for col in self._ordering.all_ordering_columns) + @property def has_total_order(self) -> bool: return isinstance(self._ordering, TotalOrdering) @@ -722,6 +747,9 @@ def _uniform_sampling(self, fraction: float) -> OrderedIR: ) def explode(self, columns: typing.Sequence[ex.DerefOp]) -> OrderedIR: + if self.order_non_deterministic: + id = bigframes.core.guid.generate_guid() + return self.promote_offsets(id) table = self._to_ibis_expr(ordering_mode="unordered", expose_hidden_cols=True) column_ids = tuple(ref.id.sql for ref in columns) @@ -1229,7 +1257,14 @@ def _bake_ordering(self) -> OrderedIR: tuple(new_exprs), self._ordering.integer_encoding, self._ordering.string_encoding, - self._ordering.total_ordering_columns, + total_ordering_columns=frozenset( + map( + ex.DerefOp, + itertools.chain.from_iterable( + col.referenced_columns for col in new_exprs + ), + ) + ), ) else: new_ordering = RowOrdering( diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 78dff26228..9e87b4b4e8 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -26,7 +26,6 @@ import bigframes.core.compile.compiled as compiled import bigframes.core.compile.concat as concat_impl -import bigframes.core.compile.default_ordering as default_ordering import bigframes.core.compile.ibis_types import bigframes.core.compile.scalar_op_compiler import bigframes.core.compile.scalar_op_compiler as compile_scalar @@ -104,10 +103,7 @@ def set_output_names( ) def compile_ordered_ir(self, node: nodes.BigFrameNode) -> compiled.OrderedIR: - ir = typing.cast(compiled.OrderedIR, self.compile_node(node, True)) - if self.strict: - assert ir.has_total_order - return ir + return typing.cast(compiled.OrderedIR, self.compile_node(node, True)) def compile_unordered_ir(self, node: nodes.BigFrameNode) -> compiled.UnorderedIR: return typing.cast(compiled.UnorderedIR, self.compile_node(node, False)) @@ -274,17 +270,6 @@ def compile_read_table_ordered( for source_id, out_id in full_mapping.items() if source_id not in visible_column_mapping ) - elif self.strict: # In strict mode, we fallback to ordering by row hash - order_values = [ - col.name(guids.generate_guid()) - for col in default_ordering.gen_default_ordering( - ibis_table, use_double_hash=True - ) - ] - ordering = bf_ordering.TotalOrdering.from_primary_key( - [value.get_name() for value in order_values] - ) - hidden_columns = tuple(order_values) else: # In unstrict mode, don't generate total ordering from hashing as this is # expensive (prevent removing any columns from table scan) @@ -316,7 +301,11 @@ def compile_filter(self, node: nodes.FilterNode, ordered: bool = True): @_compile_node.register def compile_orderby(self, node: nodes.OrderByNode, ordered: bool = True): if ordered: - return self.compile_ordered_ir(node.child).order_by(node.by) + if node.is_total_order: + # more efficient, can just discard any previous ordering and get same result + return self.compile_unordered_ir(node.child).with_total_order(node.by) + else: + return self.compile_ordered_ir(node.child).order_by(node.by) else: return self.compile_unordered_ir(node.child) diff --git a/bigframes/core/compile/default_ordering.py b/bigframes/core/compile/default_ordering.py index 178857b34c..1a1350cfd6 100644 --- a/bigframes/core/compile/default_ordering.py +++ b/bigframes/core/compile/default_ordering.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import cast +from typing import cast, Sequence import bigframes_vendored.ibis import bigframes_vendored.ibis.expr.datatypes as ibis_dtypes @@ -28,7 +28,7 @@ import bigframes.core.guid as guid -def _convert_to_nonnull_string(column: ibis_types.Column) -> ibis_types.StringValue: +def _convert_to_nonnull_string(column: ibis_types.Value) -> ibis_types.StringValue: col_type = column.type() if ( col_type.is_numeric() @@ -60,29 +60,35 @@ def _convert_to_nonnull_string(column: ibis_types.Column) -> ibis_types.StringVa ) -def gen_default_ordering( - table: ibis_types.Table, use_double_hash: bool = True -) -> list[bigframes_vendored.ibis.Value]: +def gen_row_key( + columns: Sequence[ibis_types.Value], +) -> bigframes_vendored.ibis.Value: ordering_hash_part = guid.generate_guid("bigframes_ordering_") ordering_hash_part2 = guid.generate_guid("bigframes_ordering_") ordering_rand_part = guid.generate_guid("bigframes_ordering_") # All inputs into hash must be non-null or resulting hash will be null - str_values = list( - map(lambda col: _convert_to_nonnull_string(table[col]), table.columns) - ) + str_values = list(map(_convert_to_nonnull_string, columns)) full_row_str = ( str_values[0].concat(*str_values[1:]) if len(str_values) > 1 else str_values[0] ) - full_row_hash = full_row_str.hash().name(ordering_hash_part) + full_row_hash = ( + full_row_str.hash() + .name(ordering_hash_part) + .cast(ibis_dtypes.String(nullable=True)) + ) # By modifying value slightly, we get another hash uncorrelated with the first - full_row_hash_p2 = (full_row_str + "_").hash().name(ordering_hash_part2) + full_row_hash_p2 = ( + (full_row_str + "_") + .hash() + .name(ordering_hash_part2) + .cast(ibis_dtypes.String(nullable=True)) + ) # Used to disambiguate between identical rows (which will have identical hash) - random_value = bigframes_vendored.ibis.random().name(ordering_rand_part) - - order_values = ( - [full_row_hash, full_row_hash_p2, random_value] - if use_double_hash - else [full_row_hash, random_value] + random_value = ( + bigframes_vendored.ibis.random() + .name(ordering_rand_part) + .cast(ibis_dtypes.String(nullable=True)) ) - return order_values + + return full_row_hash.concat(full_row_hash_p2, random_value) diff --git a/bigframes/core/compile/ibis_types.py b/bigframes/core/compile/ibis_types.py index a6d3949bc0..18f0834903 100644 --- a/bigframes/core/compile/ibis_types.py +++ b/bigframes/core/compile/ibis_types.py @@ -14,6 +14,7 @@ from __future__ import annotations import textwrap +import typing from typing import Any, cast, Dict, Iterable, Optional, Tuple, Union import warnings @@ -22,7 +23,7 @@ import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes import bigframes_vendored.ibis.expr.datatypes as ibis_dtypes from bigframes_vendored.ibis.expr.datatypes.core import ( - dtype as python_type_to_bigquery_type, + dtype as python_type_to_ibis_type, ) import bigframes_vendored.ibis.expr.types as ibis_types import geopandas as gpd # type: ignore @@ -472,12 +473,24 @@ class UnsupportedTypeError(ValueError): def __init__(self, type_, supported_types): self.type = type_ self.supported_types = supported_types + super().__init__( + f"'{type_}' is not one of the supported types {supported_types}" + ) def ibis_type_from_python_type(t: type) -> ibis_dtypes.DataType: if t not in bigframes.dtypes.RF_SUPPORTED_IO_PYTHON_TYPES: raise UnsupportedTypeError(t, bigframes.dtypes.RF_SUPPORTED_IO_PYTHON_TYPES) - return python_type_to_bigquery_type(t) + return python_type_to_ibis_type(t) + + +def ibis_array_output_type_from_python_type(t: type) -> ibis_dtypes.DataType: + array_of = typing.get_args(t)[0] + if array_of not in bigframes.dtypes.RF_SUPPORTED_ARRAY_OUTPUT_PYTHON_TYPES: + raise UnsupportedTypeError( + array_of, bigframes.dtypes.RF_SUPPORTED_ARRAY_OUTPUT_PYTHON_TYPES + ) + return python_type_to_ibis_type(t) def ibis_type_from_type_kind(tk: bigquery.StandardSqlTypeNames) -> ibis_dtypes.DataType: diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index d594cb3d68..4f670b51ca 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -26,6 +26,7 @@ import numpy as np import pandas as pd +import bigframes.core.compile.default_ordering import bigframes.core.compile.ibis_types import bigframes.core.expression as ex import bigframes.dtypes @@ -721,6 +722,21 @@ def strftime_op_impl(x: ibis_types.Value, op: ops.StrftimeOp): ) +@scalar_op_compiler.register_unary_op(ops.UnixSeconds) +def unix_seconds_op_impl(x: ibis_types.TimestampValue): + return x.epoch_seconds() + + +@scalar_op_compiler.register_unary_op(ops.UnixMicros) +def unix_micros_op_impl(x: ibis_types.TimestampValue): + return unix_micros(x) + + +@scalar_op_compiler.register_unary_op(ops.UnixMillis) +def unix_millis_op_impl(x: ibis_types.TimestampValue): + return unix_millis(x) + + @scalar_op_compiler.register_unary_op(ops.FloorDtOp, pass_op=True) def floor_dt_op_impl(x: ibis_types.Value, op: ops.FloorDtOp): supported_freqs = ["Y", "Q", "M", "W", "D", "h", "min", "s", "ms", "us", "ns"] @@ -1219,6 +1235,11 @@ def to_json_string_op_impl(json_obj: ibis_types.Value): return to_json_string(json_obj=json_obj) +@scalar_op_compiler.register_unary_op(ops.JSONValue, pass_op=True) +def json_value_op_impl(x: ibis_types.Value, op: ops.JSONValue): + return json_value(json_obj=x, json_path=op.json_path) + + # Blob Ops @scalar_op_compiler.register_unary_op(ops.obj_fetch_metadata_op) def obj_fetch_metadata_op_impl(obj_ref: ibis_types.Value): @@ -1839,6 +1860,17 @@ def nary_remote_function_op_impl( return result +@scalar_op_compiler.register_nary_op(ops.SqlScalarOp, pass_op=True) +def sql_scalar_op_impl(*operands: ibis_types.Value, op: ops.SqlScalarOp): + return ibis_generic.SqlScalar( + op.sql_template, + values=tuple(typing.cast(ibis_generic.Value, expr.op()) for expr in operands), + output_type=bigframes.core.compile.ibis_types.bigframes_dtype_to_ibis_dtype( + op.output_type() + ), + ).to_expr() + + @scalar_op_compiler.register_nary_op(ops.StructOp, pass_op=True) def struct_op_impl( *values: ibis_types.Value, op: ops.StructOp @@ -1850,6 +1882,11 @@ def struct_op_impl( return ibis_types.struct(data) +@scalar_op_compiler.register_nary_op(ops.RowKey, pass_op=True) +def rowkey_op_impl(*values: ibis_types.Value, op: ops.RowKey) -> ibis_types.Value: + return bigframes.core.compile.default_ordering.gen_row_key(values) + + # Helpers def is_null(value) -> bool: # float NaN/inf should be treated as distinct from 'true' null values @@ -1865,6 +1902,16 @@ def timestamp(a: str) -> ibis_dtypes.timestamp: # type: ignore """Convert string to timestamp.""" +@ibis_udf.scalar.builtin +def unix_millis(a: ibis_dtypes.timestamp) -> int: # type: ignore + """Convert a timestamp to milliseconds""" + + +@ibis_udf.scalar.builtin +def unix_micros(a: ibis_dtypes.timestamp) -> int: # type: ignore + """Convert a timestamp to microseconds""" + + # Need these because ibis otherwise tries to do casts to int that can fail @ibis_udf.scalar.builtin(name="floor") def float_floor(a: float) -> float: @@ -1925,6 +1972,13 @@ def to_json_string( # type: ignore[empty-body] """Convert JSON to STRING.""" +@ibis_udf.scalar.builtin(name="json_value") +def json_value( # type: ignore[empty-body] + json_obj: ibis_dtypes.JSON, json_path: ibis_dtypes.String +) -> ibis_dtypes.String: + """Retrieve value of a JSON field as plain STRING.""" + + @ibis_udf.scalar.builtin(name="ML.DISTANCE") def vector_distance(vector1, vector2, type: str) -> ibis_dtypes.Float64: # type: ignore[empty-body] """Computes the distance between two vectors using specified type ("EUCLIDEAN", "MANHATTAN", or "COSINE")""" diff --git a/bigframes/core/compile/single_column.py b/bigframes/core/compile/single_column.py index ef554ad499..b903f9b552 100644 --- a/bigframes/core/compile/single_column.py +++ b/bigframes/core/compile/single_column.py @@ -50,6 +50,12 @@ def join_by_column_ordered( first the coalesced join keys, then, all the left columns, and finally, all the right columns. """ + if type == "right": + if left.order_non_deterministic: + right = right._bake_ordering() + else: + if left.order_non_deterministic: + left = left._bake_ordering() # Do not reset the generator l_value_mapping = dict(zip(left.column_ids, left.column_ids)) diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py index 3b7828bbf0..2d561657cb 100644 --- a/bigframes/core/expression.py +++ b/bigframes/core/expression.py @@ -205,6 +205,10 @@ def bind_variables( def is_bijective(self) -> bool: return False + @property + def deterministic(self) -> bool: + return True + @property def is_identity(self) -> bool: """True for identity operation that does not transform input.""" @@ -409,4 +413,10 @@ def bind_refs( @property def is_bijective(self) -> bool: # TODO: Mark individual functions as bijective? - return False + return all(input.is_bijective for input in self.inputs) and self.op.is_bijective + + @property + def deterministic(self) -> bool: + return ( + all(input.deterministic for input in self.inputs) and self.op.deterministic + ) diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index dfbe2ddea2..5fb5fb14d2 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -15,7 +15,7 @@ from __future__ import annotations import typing -from typing import Sequence, Union +from typing import Sequence, Tuple, Union import bigframes_vendored.constants as constants import bigframes_vendored.pandas.core.groupby as vendored_pandas_groupby @@ -26,6 +26,7 @@ import bigframes.core as core import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks +import bigframes.core.expression import bigframes.core.ordering as order import bigframes.core.utils as utils import bigframes.core.validations as validations @@ -334,24 +335,19 @@ def agg(self, func=None, **kwargs) -> typing.Union[df.DataFrame, series.Series]: return self._agg_named(**kwargs) def _agg_string(self, func: str) -> df.DataFrame: - aggregations = [ - (col_id, agg_ops.lookup_agg_func(func)) - for col_id in self._aggregated_columns() - ] + ids, labels = self._aggregated_columns() + aggregations = [agg(col_id, agg_ops.lookup_agg_func(func)) for col_id in ids] agg_block, _ = self._block.aggregate( by_column_ids=self._by_col_ids, aggregations=aggregations, dropna=self._dropna, + column_labels=labels, ) dataframe = df.DataFrame(agg_block) return dataframe if self._as_index else self._convert_index(dataframe) def _agg_dict(self, func: typing.Mapping) -> df.DataFrame: - aggregations: typing.List[ - typing.Tuple[ - str, typing.Union[agg_ops.UnaryAggregateOp, agg_ops.NullaryAggregateOp] - ] - ] = [] + aggregations: typing.List[bigframes.core.expression.Aggregation] = [] column_labels = [] want_aggfunc_level = any(utils.is_list_like(aggs) for aggs in func.values()) @@ -362,7 +358,7 @@ def _agg_dict(self, func: typing.Mapping) -> df.DataFrame: funcs_for_id if utils.is_list_like(funcs_for_id) else [funcs_for_id] ) for f in func_list: - aggregations.append((col_id, agg_ops.lookup_agg_func(f))) + aggregations.append(agg(col_id, agg_ops.lookup_agg_func(f))) column_labels.append(label) agg_block, _ = self._block.aggregate( by_column_ids=self._by_col_ids, @@ -373,7 +369,10 @@ def _agg_dict(self, func: typing.Mapping) -> df.DataFrame: agg_block = agg_block.with_column_labels( utils.combine_indices( pd.Index(column_labels), - pd.Index(agg[1].name for agg in aggregations), + pd.Index( + typing.cast(agg_ops.AggregateOp, agg.op).name + for agg in aggregations + ), ) ) else: @@ -382,34 +381,21 @@ def _agg_dict(self, func: typing.Mapping) -> df.DataFrame: return dataframe if self._as_index else self._convert_index(dataframe) def _agg_list(self, func: typing.Sequence) -> df.DataFrame: + ids, labels = self._aggregated_columns() aggregations = [ - (col_id, agg_ops.lookup_agg_func(f)) - for col_id in self._aggregated_columns() - for f in func + agg(col_id, agg_ops.lookup_agg_func(f)) for col_id in ids for f in func ] if self._block.column_labels.nlevels > 1: # Restructure MultiIndex for proper format: (idx1, idx2, func) # rather than ((idx1, idx2), func). - aggregated_columns = pd.MultiIndex.from_tuples( - [ - self._block.col_id_to_label[col_id] - for col_id in self._aggregated_columns() - ], - names=[*self._block.column_labels.names], - ).to_frame(index=False) - column_labels = [ - tuple(col_id) + (f,) - for col_id in aggregated_columns.to_numpy() - for f in func - ] - else: - column_labels = [ - (self._block.col_id_to_label[col_id], f) - for col_id in self._aggregated_columns() + tuple(label) + (f,) + for label in labels.to_frame(index=False).to_numpy() for f in func ] + else: # Single-level index + column_labels = [(label, f) for label in labels for f in func] agg_block, _ = self._block.aggregate( by_column_ids=self._by_col_ids, @@ -435,7 +421,7 @@ def _agg_named(self, **kwargs) -> df.DataFrame: if not isinstance(v, tuple) or (len(v) != 2): raise TypeError("kwargs values must be 2-tuples of column, aggfunc") col_id = self._resolve_label(v[0]) - aggregations.append((col_id, agg_ops.lookup_agg_func(v[1]))) + aggregations.append(agg(col_id, agg_ops.lookup_agg_func(v[1]))) column_labels.append(k) agg_block, _ = self._block.aggregate( by_column_ids=self._by_col_ids, @@ -470,15 +456,19 @@ def _raise_on_non_numeric(self, op: str): ) return self - def _aggregated_columns(self, numeric_only: bool = False) -> typing.Sequence[str]: + def _aggregated_columns( + self, numeric_only: bool = False + ) -> Tuple[typing.Sequence[str], pd.Index]: valid_agg_cols: list[str] = [] - for col_id in self._selected_cols: + offsets: list[int] = [] + for i, col_id in enumerate(self._block.value_columns): is_numeric = ( self._column_type(col_id) in dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE ) - if is_numeric or not numeric_only: + if (col_id in self._selected_cols) and (is_numeric or not numeric_only): + offsets.append(i) valid_agg_cols.append(col_id) - return valid_agg_cols + return valid_agg_cols, self._block.column_labels.take(offsets) def _column_type(self, col_id: str) -> dtypes.Dtype: col_offset = self._block.value_columns.index(col_id) @@ -488,11 +478,12 @@ def _column_type(self, col_id: str) -> dtypes.Dtype: def _aggregate_all( self, aggregate_op: agg_ops.UnaryAggregateOp, numeric_only: bool = False ) -> df.DataFrame: - aggregated_col_ids = self._aggregated_columns(numeric_only=numeric_only) - aggregations = [(col_id, aggregate_op) for col_id in aggregated_col_ids] + aggregated_col_ids, labels = self._aggregated_columns(numeric_only=numeric_only) + aggregations = [agg(col_id, aggregate_op) for col_id in aggregated_col_ids] result_block, _ = self._block.aggregate( by_column_ids=self._by_col_ids, aggregations=aggregations, + column_labels=labels, dropna=self._dropna, ) dataframe = df.DataFrame(result_block) @@ -508,7 +499,7 @@ def _apply_window_op( window_spec = window or window_specs.cumulative_rows( grouping_keys=tuple(self._by_col_ids) ) - columns = self._aggregated_columns(numeric_only=numeric_only) + columns, _ = self._aggregated_columns(numeric_only=numeric_only) block, result_ids = self._block.multi_apply_window_op( columns, op, window_spec=window_spec ) @@ -639,11 +630,11 @@ def prod(self, *args) -> series.Series: def agg(self, func=None) -> typing.Union[df.DataFrame, series.Series]: column_names: list[str] = [] if isinstance(func, str): - aggregations = [(self._value_column, agg_ops.lookup_agg_func(func))] + aggregations = [agg(self._value_column, agg_ops.lookup_agg_func(func))] column_names = [func] elif utils.is_list_like(func): aggregations = [ - (self._value_column, agg_ops.lookup_agg_func(f)) for f in func + agg(self._value_column, agg_ops.lookup_agg_func(f)) for f in func ] column_names = list(func) else: @@ -756,7 +747,7 @@ def expanding(self, min_periods: int = 1) -> windows.Window: def _aggregate(self, aggregate_op: agg_ops.UnaryAggregateOp) -> series.Series: result_block, _ = self._block.aggregate( self._by_col_ids, - ((self._value_column, aggregate_op),), + (agg(self._value_column, aggregate_op),), dropna=self._dropna, ) @@ -781,3 +772,13 @@ def _apply_window_op( window_spec=window_spec, ) return series.Series(block.select_column(result_id)) + + +def agg(input: str, op: agg_ops.AggregateOp) -> bigframes.core.expression.Aggregation: + if isinstance(op, agg_ops.UnaryAggregateOp): + return bigframes.core.expression.UnaryAggregation( + op, bigframes.core.expression.deref(input) + ) + else: + assert isinstance(op, agg_ops.NullaryAggregateOp) + return bigframes.core.expression.NullaryAggregation(op) diff --git a/bigframes/core/log_adapter.py b/bigframes/core/log_adapter.py index cc7d4ad58d..36aa6682bd 100644 --- a/bigframes/core/log_adapter.py +++ b/bigframes/core/log_adapter.py @@ -13,15 +13,21 @@ # limitations under the License. import functools +import inspect import threading from typing import List +from google.cloud import bigquery +import pandas + _lock = threading.Lock() # The limit is 64 (https://cloud.google.com/bigquery/docs/labels-intro#requirements), # but leave a few spare for internal labels to be added. # See internal issue 386825477. MAX_LABELS_COUNT = 64 - 8 +PANDAS_API_TRACKING_TASK = "pandas_api_tracking" +PANDAS_PARAM_TRACKING_TASK = "pandas_param_tracking" _api_methods: List = [] _excluded_methods = ["__setattr__", "__getattr__"] @@ -30,11 +36,86 @@ _call_stack: List = [] +def submit_pandas_labels( + bq_client: bigquery.Client, + class_name: str, + method_name: str, + args=(), + kwargs={}, + task: str = PANDAS_API_TRACKING_TASK, +): + """ + Submits usage of API to BigQuery using a simulated failed query. + + This function is designed to capture and log details about the usage of pandas methods, + including class and method names, the count of positional arguments, and any keyword + arguments that match the method's signature. To avoid incurring costs, it simulates a + query execution using a query with syntax errors. + + Args: + bq_client (bigquery.Client): The client used to interact with BigQuery. + class_name (str): The name of the pandas class being used. + method_name (str): The name of the method being invoked. + args (tuple): The positional arguments passed to the method. + kwargs (dict): The keyword arguments passed to the method. + task (str): The specific task type for the logging event: + - 'PANDAS_API_TRACKING_TASK': Indicates that the unimplemented feature is a method. + - 'PANDAS_PARAM_TRACKING_TASK': Indicates that the unimplemented feature is a + parameter of a method. + """ + labels_dict = { + "task": task, + "class_name": class_name.lower(), + "method_name": method_name.lower(), + "args_count": len(args), + } + + if hasattr(pandas, class_name): + cls = getattr(pandas, class_name) + else: + return + + if hasattr(cls, method_name): + method = getattr(cls, method_name) + else: + return + + if kwargs: + # Iterate through the keyword arguments and add them to the labels dictionary if they + # are parameters that are implemented in pandas and the maximum label count has not been reached. + signature = inspect.signature(method) + param_names = [param.name for param in signature.parameters.values()] + + idx = 0 + for key in kwargs.keys(): + if len(labels_dict) >= MAX_LABELS_COUNT: + break + if key in param_names: + labels_dict[f"kwargs_{idx}"] = key.lower() + idx += 1 + + # If this log is for tracking unimplemented parameters and no keyword arguments were + # provided, skip logging. + if len(labels_dict) == 4 and task == PANDAS_PARAM_TRACKING_TASK: + return + + # Run a query with syntax error to avoid cost. + query = "SELECT COUNT(x FROM data_table—" + job_config = bigquery.QueryJobConfig(labels=labels_dict) + bq_client.query(query, job_config=job_config) + + def class_logger(decorated_cls): """Decorator that adds logging functionality to each method of the class.""" for attr_name, attr_value in decorated_cls.__dict__.items(): if callable(attr_value) and (attr_name not in _excluded_methods): - setattr(decorated_cls, attr_name, method_logger(attr_value, decorated_cls)) + if isinstance(attr_value, staticmethod): + # TODO(b/390244171) support for staticmethod + pass + else: + setattr( + decorated_cls, attr_name, method_logger(attr_value, decorated_cls) + ) elif isinstance(attr_value, property): setattr( decorated_cls, attr_name, property_logger(attr_value, decorated_cls) @@ -46,7 +127,7 @@ def method_logger(method, decorated_cls): """Decorator that adds logging functionality to a method.""" @functools.wraps(method) - def wrapper(*args, **kwargs): + def wrapper(self, *args, **kwargs): class_name = decorated_cls.__name__ # Access decorated class name api_method_name = str(method.__name__) full_method_name = f"{class_name.lower()}-{api_method_name}" @@ -58,7 +139,23 @@ def wrapper(*args, **kwargs): _call_stack.append(full_method_name) try: - return method(*args, **kwargs) + return method(self, *args, **kwargs) + except (NotImplementedError, TypeError) as e: + # Log method parameters that are implemented in pandas but either missing (TypeError) + # or not fully supported (NotImplementedError) in BigFrames. + # Logging is currently supported only when we can access the bqclient through + # self._block.expr.session.bqclient. Also, to avoid generating multiple queries + # because of internal calls, we log only when the method is directly invoked. + if hasattr(self, "_block") and len(_call_stack) == 1: + submit_pandas_labels( + self._block.expr.session.bqclient, + class_name, + api_method_name, + args, + kwargs, + task=PANDAS_PARAM_TRACKING_TASK, + ) + raise e finally: _call_stack.pop() diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 244f1e7751..fe79da2bf6 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -976,6 +976,9 @@ def remap_refs(self, mappings: Mapping[bfet_ids.ColumnId, bfet_ids.ColumnId]): @dataclasses.dataclass(frozen=True, eq=False) class OrderByNode(UnaryNode): by: Tuple[OrderingExpression, ...] + # This is an optimization, if true, can discard previous orderings. + # might be a total ordering even if false + is_total_order: bool = False @property def variables_introduced(self) -> int: diff --git a/bigframes/core/ordering.py b/bigframes/core/ordering.py index 059987d597..357cc8145c 100644 --- a/bigframes/core/ordering.py +++ b/bigframes/core/ordering.py @@ -59,6 +59,10 @@ class OrderingExpression: def referenced_columns(self) -> Set[ids.ColumnId]: return set(self.scalar_expression.column_references) + @property + def deterministic(self) -> bool: + return self.scalar_expression.deterministic + def remap_column_refs( self, mapping: Mapping[ids.ColumnId, ids.ColumnId], diff --git a/bigframes/core/sql.py b/bigframes/core/sql.py index ae0a0de2aa..f4de177f37 100644 --- a/bigframes/core/sql.py +++ b/bigframes/core/sql.py @@ -18,10 +18,13 @@ """ import datetime +import decimal import json import math from typing import cast, Collection, Iterable, Mapping, Optional, TYPE_CHECKING, Union +import shapely # type: ignore + import bigframes.core.compile.googlesql as googlesql if TYPE_CHECKING: @@ -31,12 +34,16 @@ ### Writing SQL Values (literals, column references, table references, etc.) -def simple_literal(value: str | int | bool | float | datetime.datetime): +def simple_literal(value: bytes | str | int | bool | float | datetime.datetime | None): """Return quoted input string.""" # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#literals - if isinstance(value, str): + if value is None: + return "NULL" + elif isinstance(value, str): # Single quoting seems to work nicer with ibis than double quoting return f"'{googlesql._escape_chars(value)}'" + elif isinstance(value, bytes): + return repr(value) elif isinstance(value, (bool, int)): return str(value) elif isinstance(value, float): @@ -48,8 +55,21 @@ def simple_literal(value: str | int | bool | float | datetime.datetime): if value == -math.inf: return 'CAST("-inf" as FLOAT)' return str(value) - if isinstance(value, datetime.datetime): - return f"TIMESTAMP('{value.isoformat()}')" + # Check datetime first as it is a subclass of date + elif isinstance(value, datetime.datetime): + if value.tzinfo is None: + return f"DATETIME('{value.isoformat()}')" + else: + return f"TIMESTAMP('{value.isoformat()}')" + elif isinstance(value, datetime.date): + return f"DATE('{value.isoformat()}')" + elif isinstance(value, datetime.time): + return f"TIME(DATETIME('1970-01-01 {value.isoformat()}'))" + elif isinstance(value, shapely.Geometry): + return f"ST_GEOGFROMTEXT({simple_literal(shapely.to_wkt(value))})" + elif isinstance(value, decimal.Decimal): + # TODO: disambiguate BIGNUMERIC based on scale and/or precision + return f"CAST('{str(value)}' AS NUMERIC)" else: raise ValueError(f"Cannot produce literal for {value}") diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 01e9bd6308..ce57661919 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -19,6 +19,7 @@ import datetime import inspect import itertools +import json import re import sys import textwrap @@ -48,7 +49,6 @@ import pyarrow import tabulate -import bigframes import bigframes._config.display_options as display_options import bigframes.constants import bigframes.core @@ -180,9 +180,7 @@ def __init__( if columns: block = block.select_columns(list(columns)) # type:ignore if dtype: - block = block.multi_apply_unary_op( - block.value_columns, ops.AsTypeOp(to_type=dtype) - ) + block = block.multi_apply_unary_op(ops.AsTypeOp(to_type=dtype)) self._block = block else: @@ -645,6 +643,9 @@ def __getattr__(self, key: str): return self.__getitem__(key) if hasattr(pandas.DataFrame, key): + log_adapter.submit_pandas_labels( + self._block.expr.session.bqclient, self.__class__.__name__, key + ) raise AttributeError( textwrap.dedent( f""" @@ -741,16 +742,14 @@ def _repr_html_(self) -> str: df = self.copy() if bigframes.options.experiments.blob: - import bigframes.bigquery as bbq - blob_cols = [ col for col in df.columns if df[col].dtype == bigframes.dtypes.OBJ_REF_DTYPE ] for col in blob_cols: - df[col] = df[col]._apply_unary_op(ops.ObjGetAccessUrl(mode="R")) - df[col] = bbq.json_extract(df[col], "$.access_urls.read_url") + # TODO(garrettwu): Not necessary to get access urls for all the rows. Update when having a to get URLs from local data. + df[col] = df[col].blob._get_runtime(mode="R", with_metadata=True) # TODO(swast): pass max_columns and get the true column count back. Maybe # get 1 more column than we have requested so that pandas can add the @@ -767,11 +766,21 @@ def _repr_html_(self) -> str: # Allows to preview images in the DataFrame. The implementation changes the string repr as well, that it doesn't truncate strings or escape html charaters such as "<" and ">". We may need to implement a full-fledged repr module to better support types not in pandas. if bigframes.options.experiments.blob: - def url_to_image_html(url: str) -> str: - # url is a json string, which already contains double-quotes "" - return f"" + def obj_ref_rt_to_html(obj_ref_rt) -> str: + obj_ref_rt_json = json.loads(obj_ref_rt) + content_type = typing.cast( + str, + obj_ref_rt_json["objectref"]["details"]["gcs_metadata"][ + "content_type" + ], + ) + if content_type.startswith("image"): + url = obj_ref_rt_json["access_urls"]["read_url"] + return f'' + + return f'uri: {obj_ref_rt_json["objectref"]["uri"]}, authorizer: {obj_ref_rt_json["objectref"]["authorizer"]}' - formatters = {blob_col: url_to_image_html for blob_col in blob_cols} + formatters = {blob_col: obj_ref_rt_to_html for blob_col in blob_cols} # set max_colwidth so not to truncate the image url with pandas.option_context("display.max_colwidth", None): @@ -844,9 +853,7 @@ def _apply_scalar_binop( left_input=ex.free_var("var1"), right_input=ex.const(other), ) - return DataFrame( - self._block.multi_apply_unary_op(self._block.value_columns, expr) - ) + return DataFrame(self._block.multi_apply_unary_op(expr)) def _apply_series_binop_axis_0( self, @@ -1976,6 +1983,11 @@ def sort_values( kind: str = "quicksort", na_position: typing.Literal["first", "last"] = "last", ) -> DataFrame: + if isinstance(by, (bigframes.series.Series, indexes.Index, DataFrame)): + raise KeyError( + f"Invalid key type: {type(by).__name__}. Please provide valid column name(s)." + ) + if na_position not in {"first", "last"}: raise ValueError("Param na_position must be one of 'first' or 'last'") @@ -2394,9 +2406,7 @@ def dropna( result = result.reset_index() return DataFrame(result) else: - isnull_block = self._block.multi_apply_unary_op( - self._block.value_columns, ops.isnull_op - ) + isnull_block = self._block.multi_apply_unary_op(ops.isnull_op) if how == "any": null_locations = DataFrame(isnull_block).any().to_pandas() else: # 'all' @@ -3033,8 +3043,15 @@ def merge( return DataFrame(block) def join( - self, other: DataFrame, *, on: Optional[str] = None, how: str = "left" + self, + other: Union[DataFrame, bigframes.series.Series], + *, + on: Optional[str] = None, + how: str = "left", ) -> DataFrame: + if isinstance(other, bigframes.series.Series): + other = other.to_frame() + left, right = self, other if not left.columns.intersection(right.columns).empty: @@ -3822,7 +3839,7 @@ def to_orc(self, path=None, **kwargs) -> bytes | None: return as_pandas_default_index.to_orc(path, **kwargs) def _apply_unary_op(self, operation: ops.UnaryOp) -> DataFrame: - block = self._block.multi_apply_unary_op(self._block.value_columns, operation) + block = self._block.multi_apply_unary_op(operation) return DataFrame(block) def _map_clustering_columns( @@ -3917,6 +3934,10 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: ) def apply(self, func, *, axis=0, args: typing.Tuple = (), **kwargs): + # In Bigframes remote function, DataFrame '.apply' method is specifically + # designed to work with row-wise or column-wise operations, where the input + # to the applied function should be a Series, not a scalar. + if utils.get_axis_number(axis) == 1: msg = "axis=1 scenario is in preview." warnings.warn(msg, category=bfe.PreviewWarning) @@ -4009,10 +4030,34 @@ def apply(self, func, *, axis=0, args: typing.Tuple = (), **kwargs): ops.NaryRemoteFunctionOp(func=func), series_list[1:] ) result_series.name = None + + # if the output is an array, reconstruct it from the json serialized + # string form + if bigframes.dtypes.is_array_like(func.output_dtype): + import bigframes.bigquery as bbq + + result_dtype = bigframes.dtypes.arrow_dtype_to_bigframes_dtype( + func.output_dtype.pyarrow_dtype.value_type + ) + result_series = bbq.json_extract_string_array( + result_series, value_dtype=result_dtype + ) + return result_series + # At this point column-wise or element-wise remote function operation will + # be performed (not supported). + if hasattr(func, "bigframes_remote_function"): + raise NotImplementedError( + "BigFrames DataFrame '.apply()' does not support remote function " + "for column-wise (i.e. with axis=0) operations, please use a " + "regular python function instead. For element-wise operations of " + "the remote function, please use '.map()'." + ) + # Per-column apply results = {name: func(col, *args, **kwargs) for name, col in self.items()} + if all( [ isinstance(val, bigframes.series.Series) or utils.is_list_like(val) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 6e179225ea..3da3fa24f3 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -18,7 +18,7 @@ import datetime import decimal import typing -from typing import Dict, List, Literal, Union +from typing import Any, Dict, List, Literal, Union import bigframes_vendored.constants as constants import geopandas as gpd # type: ignore @@ -26,6 +26,7 @@ import numpy as np import pandas as pd import pyarrow as pa +import shapely # type: ignore # Type hints for Pandas dtypes supported by BigQuery DataFrame Dtype = Union[ @@ -450,6 +451,74 @@ def bigframes_dtype_to_arrow_dtype( ) +def bigframes_dtype_to_literal( + bigframes_dtype: Dtype, +) -> Any: + """Create a representative literal value for a bigframes dtype. + + The inverse of infer_literal_type(). + """ + if isinstance(bigframes_dtype, pd.ArrowDtype): + arrow_type = bigframes_dtype.pyarrow_dtype + return arrow_type_to_literal(arrow_type) + + if isinstance(bigframes_dtype, pd.Float64Dtype): + return 1.0 + if isinstance(bigframes_dtype, pd.Int64Dtype): + return 1 + if isinstance(bigframes_dtype, pd.BooleanDtype): + return True + if isinstance(bigframes_dtype, pd.StringDtype): + return "string" + if isinstance(bigframes_dtype, gpd.array.GeometryDtype): + return shapely.Point((0, 0)) + + raise ValueError( + f"No literal conversion for {bigframes_dtype}. {constants.FEEDBACK_LINK}" + ) + + +def arrow_type_to_literal( + arrow_type: pa.DataType, +) -> Any: + """Create a representative literal value for an arrow type.""" + if pa.types.is_list(arrow_type): + return [arrow_type_to_literal(arrow_type.value_type)] + if pa.types.is_struct(arrow_type): + return { + field.name: arrow_type_to_literal(field.type) for field in arrow_type.fields + } + if pa.types.is_string(arrow_type): + return "string" + if pa.types.is_binary(arrow_type): + return b"bytes" + if pa.types.is_floating(arrow_type): + return 1.0 + if pa.types.is_integer(arrow_type): + return 1 + if pa.types.is_boolean(arrow_type): + return True + if pa.types.is_date(arrow_type): + return datetime.date(2025, 1, 1) + if pa.types.is_timestamp(arrow_type): + return datetime.datetime( + 2025, + 1, + 1, + 1, + 1, + tzinfo=datetime.timezone.utc if arrow_type.tz is not None else None, + ) + if pa.types.is_decimal(arrow_type): + return decimal.Decimal("1.0") + if pa.types.is_time(arrow_type): + return datetime.time(1, 1, 1) + + raise ValueError( + f"No literal conversion for {arrow_type}. {constants.FEEDBACK_LINK}" + ) + + def infer_literal_type(literal) -> typing.Optional[Dtype]: # Maybe also normalize literal to canonical python representation to remove this burden from compilers? if pd.api.types.is_list_like(literal): @@ -701,6 +770,13 @@ def lcd_type_or_throw(dtype1: Dtype, dtype2: Dtype) -> Dtype: # https://cloud.google.com/bigquery/docs/remote-functions#limitations RF_SUPPORTED_IO_PYTHON_TYPES = {bool, bytes, float, int, str} +# Support array output types in BigQuery DataFrames remote functions even though +# it is not currently (2024-10-06) supported in BigQuery remote functions. +# https://cloud.google.com/bigquery/docs/remote-functions#limitations +# TODO(b/284515241): remove this special handling when BigQuery remote functions +# support array. +RF_SUPPORTED_ARRAY_OUTPUT_PYTHON_TYPES = {bool, float, int, str} + RF_SUPPORTED_IO_BIGQUERY_TYPEKINDS = { "BOOLEAN", "BOOL", diff --git a/bigframes/functions/_remote_function_client.py b/bigframes/functions/_remote_function_client.py index 5acd31b425..0d0cc08128 100644 --- a/bigframes/functions/_remote_function_client.py +++ b/bigframes/functions/_remote_function_client.py @@ -95,6 +95,7 @@ def create_bq_remote_function( endpoint, bq_function_name, max_batching_rows, + metadata, ): """Create a BigQuery remote function given the artifacts of a user defined function and the http endpoint of a corresponding cloud function.""" @@ -120,9 +121,14 @@ def create_bq_remote_function( "max_batching_rows": max_batching_rows, } + if metadata: + # We are using the description field to store this structured + # bigframes specific metadata for the lack of a better option + remote_function_options["description"] = metadata + remote_function_options_str = ", ".join( [ - f'{key}="{val}"' if isinstance(val, str) else f"{key}={val}" + f"{key}='{val}'" if isinstance(val, str) else f"{key}={val}" for key, val in remote_function_options.items() if val is not None ] @@ -200,14 +206,7 @@ def generate_cloud_function_code( package_requirements=None, is_row_processor=False, ): - """Generate the cloud function code for a given user defined function. - - Args: - input_types (tuple[str]): - Types of the input arguments in BigQuery SQL data type names. - output_type (str): - Types of the output scalar as a BigQuery SQL data type name. - """ + """Generate the cloud function code for a given user defined function.""" # requirements.txt if package_requirements: @@ -240,14 +239,7 @@ def create_cloud_function( memory_mib=1024, ingress_settings="all", ): - """Create a cloud function from the given user defined function. - - Args: - input_types (tuple[str]): - Types of the input arguments in BigQuery SQL data type names. - output_type (str): - Types of the output scalar as a BigQuery SQL data type name. - """ + """Create a cloud function from the given user defined function.""" # Build and deploy folder structure containing cloud function with tempfile.TemporaryDirectory() as directory: @@ -394,6 +386,7 @@ def provision_bq_remote_function( cloud_function_vpc_connector, cloud_function_memory_mib, cloud_function_ingress_settings, + bq_metadata, ): """Provision a BigQuery remote function.""" # Augment user package requirements with any internal package @@ -473,6 +466,7 @@ def provision_bq_remote_function( cf_endpoint, remote_function_name, max_batching_rows, + bq_metadata, ) created_new = True diff --git a/bigframes/functions/_remote_function_session.py b/bigframes/functions/_remote_function_session.py index 662c32a6a6..d6b729bf6e 100644 --- a/bigframes/functions/_remote_function_session.py +++ b/bigframes/functions/_remote_function_session.py @@ -34,6 +34,7 @@ import bigframes_vendored.constants as constants import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes +import bigframes_vendored.ibis.expr.datatypes as ibis_dtypes import bigframes_vendored.ibis.expr.operations.udf as ibis_udf import cloudpickle import google.api_core.exceptions @@ -167,12 +168,19 @@ def remote_function( `$ gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID" --role="roles/run.invoker"`. Args: - input_types (None, type, or sequence(type)): + input_types (type or sequence(type), Optional): For scalar user defined function it should be the input type or - sequence of input types. For row processing user defined function, - type `Series` should be specified. - output_type (Optional[type]): - Data type of the output in the user defined function. + sequence of input types. The supported scalar input types are + `bool`, `bytes`, `float`, `int`, `str`. For row processing user + defined function (i.e. functions that receive a single input + representing a row in form of a Series), type `Series` should be + specified. + output_type (type, Optional): + Data type of the output in the user defined function. If the + user defined function returns an array, then `list[type]` should + be specified. The supported output types are `bool`, `bytes`, + `float`, `int`, `str`, `list[bool]`, `list[float]`, `list[int]` + and `list[str]`. session (bigframes.Session, Optional): BigQuery DataFrames session to use for getting default project, dataset and BigQuery connection. @@ -497,6 +505,24 @@ def try_delattr(attr): try_delattr("is_row_processor") try_delattr("ibis_node") + # resolve the output type that can be supported in the bigframes, + # ibis, BQ remote functions and cloud functions integration + ibis_output_type_for_bqrf = ibis_signature.output_type + bqrf_metadata = None + if isinstance(ibis_signature.output_type, ibis_dtypes.Array): + # TODO(b/284515241): remove this special handling to support + # array output types once BQ remote functions support ARRAY. + # Until then, use json serialized strings at the cloud function + # and BQ level, and parse that to the intended output type at + # the bigframes level. + ibis_output_type_for_bqrf = ibis_dtypes.String() + bqrf_metadata = _utils.get_bigframes_metadata( + python_output_type=output_type + ) + bqrf_output_type = third_party_ibis_bqtypes.BigQueryType.from_ibis( + ibis_output_type_for_bqrf + ) + ( rf_name, cf_name, @@ -508,9 +534,7 @@ def try_delattr(attr): for type_ in ibis_signature.input_types if type_ is not None ), - output_type=third_party_ibis_bqtypes.BigQueryType.from_ibis( - ibis_signature.output_type - ), + output_type=bqrf_output_type, reuse=reuse, name=name, package_requirements=packages, @@ -521,6 +545,7 @@ def try_delattr(attr): cloud_function_vpc_connector=cloud_function_vpc_connector, cloud_function_memory_mib=cloud_function_memory_mib, cloud_function_ingress_settings=cloud_function_ingress_settings, + bq_metadata=bqrf_metadata, ) # TODO(shobs): Find a better way to support udfs with param named "name". @@ -541,7 +566,7 @@ def try_delattr(attr): name=rf_name, catalog=dataset_ref.project, database=dataset_ref.dataset_id, - signature=(ibis_signature.input_types, ibis_signature.output_type), + signature=(ibis_signature.input_types, ibis_output_type_for_bqrf), ) # type: ignore func.bigframes_cloud_function = ( remote_function_client.get_cloud_function_fully_qualified_name(cf_name) diff --git a/bigframes/functions/_utils.py b/bigframes/functions/_utils.py index c50e55cc33..591da01dd0 100644 --- a/bigframes/functions/_utils.py +++ b/bigframes/functions/_utils.py @@ -15,6 +15,8 @@ import hashlib import inspect +import json +import typing from typing import cast, List, NamedTuple, Optional, Sequence, Set import bigframes_vendored.ibis.expr.datatypes.core as ibis_dtypes @@ -26,6 +28,7 @@ import pyarrow import bigframes.core.compile.ibis_types +import bigframes.dtypes # Naming convention for the remote function artifacts _BIGFRAMES_REMOTE_FUNCTION_PREFIX = "bigframes" @@ -194,6 +197,7 @@ class IbisSignature(NamedTuple): parameter_names: List[str] input_types: List[Optional[ibis_dtypes.DataType]] output_type: ibis_dtypes.DataType + output_type_override: Optional[ibis_dtypes.DataType] = None def ibis_signature_from_python_signature( @@ -202,13 +206,77 @@ def ibis_signature_from_python_signature( output_type: type, ) -> IbisSignature: + ibis_input_types: List[Optional[ibis_dtypes.DataType]] = [ + bigframes.core.compile.ibis_types.ibis_type_from_python_type(t) + for t in input_types + ] + + if typing.get_origin(output_type) is list: + ibis_output_type = ( + bigframes.core.compile.ibis_types.ibis_array_output_type_from_python_type( + output_type + ) + ) + else: + ibis_output_type = bigframes.core.compile.ibis_types.ibis_type_from_python_type( + output_type + ) + return IbisSignature( parameter_names=list(signature.parameters.keys()), - input_types=[ - bigframes.core.compile.ibis_types.ibis_type_from_python_type(t) - for t in input_types - ], - output_type=bigframes.core.compile.ibis_types.ibis_type_from_python_type( - output_type - ), + input_types=ibis_input_types, + output_type=ibis_output_type, ) + + +def get_python_output_type_from_bigframes_metadata( + metadata_text: str, +) -> Optional[type]: + try: + metadata_dict = json.loads(metadata_text) + except (TypeError, json.decoder.JSONDecodeError): + return None + + try: + output_type = metadata_dict["value"]["python_array_output_type"] + except KeyError: + return None + + for ( + python_output_array_type + ) in bigframes.dtypes.RF_SUPPORTED_ARRAY_OUTPUT_PYTHON_TYPES: + if python_output_array_type.__name__ == output_type: + return list[python_output_array_type] # type: ignore + + return None + + +def get_bigframes_metadata(*, python_output_type: Optional[type] = None) -> str: + # Let's keep the actual metadata inside one level of nesting so that in + # future we can use a top level key "version" (parallel to "value"), based + # on which "value" can be interpreted according to the "version". The + # absence of "version" should be interpreted as default version. + inner_metadata = {} + if typing.get_origin(python_output_type) is list: + python_output_array_type = typing.get_args(python_output_type)[0] + if ( + python_output_array_type + in bigframes.dtypes.RF_SUPPORTED_ARRAY_OUTPUT_PYTHON_TYPES + ): + inner_metadata[ + "python_array_output_type" + ] = python_output_array_type.__name__ + + metadata = {"value": inner_metadata} + metadata_ser = json.dumps(metadata) + + # let's make sure the serialized value is deserializable + if ( + get_python_output_type_from_bigframes_metadata(metadata_ser) + != python_output_type + ): + raise ValueError( + f"python_output_type {python_output_type} is not serializable." + ) + + return metadata_ser diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 9b68843a7d..533c93e7cb 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -16,9 +16,11 @@ import inspect import logging +import typing from typing import cast, Optional, TYPE_CHECKING import warnings +import bigframes_vendored.ibis.expr.datatypes as ibis_dtypes import bigframes_vendored.ibis.expr.operations.udf as ibis_udf if TYPE_CHECKING: @@ -53,9 +55,30 @@ class ReturnTypeMissingError(ValueError): # TODO: Move this to compile folder def ibis_signature_from_routine(routine: bigquery.Routine) -> _utils.IbisSignature: - if not routine.return_type: + if routine.return_type: + ibis_output_type = bigframes.core.compile.ibis_types.ibis_type_from_type_kind( + routine.return_type.type_kind + ) + else: raise ReturnTypeMissingError + ibis_output_type_override: Optional[ibis_dtypes.DataType] = None + if python_output_type := _utils.get_python_output_type_from_bigframes_metadata( + routine.description + ): + if not isinstance(ibis_output_type, ibis_dtypes.String): + raise TypeError( + "An explicit output_type should be provided only for a BigQuery function with STRING output." + ) + if typing.get_origin(python_output_type) is list: + ibis_output_type_override = bigframes.core.compile.ibis_types.ibis_array_output_type_from_python_type( + cast(type, python_output_type) + ) + else: + raise TypeError( + "Currently only list of a type is supported as python output type." + ) + return _utils.IbisSignature( parameter_names=[arg.name for arg in routine.arguments], input_types=[ @@ -66,9 +89,8 @@ def ibis_signature_from_routine(routine: bigquery.Routine) -> _utils.IbisSignatu else None for arg in routine.arguments ], - output_type=bigframes.core.compile.ibis_types.ibis_type_from_type_kind( - routine.return_type.type_kind - ), + output_type=ibis_output_type, + output_type_override=ibis_output_type_override, ) @@ -206,8 +228,11 @@ def func(*bigframes_args, **bigframes_kwargs): func.input_dtypes = tuple(function_input_dtypes) # type: ignore func.output_dtype = bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( # type: ignore - ibis_signature.output_type + ibis_signature.output_type_override + if ibis_signature.output_type_override + else ibis_signature.output_type ) + func.is_row_processor = is_row_processor # type: ignore func.ibis_node = node # type: ignore return func diff --git a/bigframes/functions/remote_function_template.py b/bigframes/functions/remote_function_template.py index c666f41daa..0809baf5cc 100644 --- a/bigframes/functions/remote_function_template.py +++ b/bigframes/functions/remote_function_template.py @@ -169,6 +169,10 @@ def udf_http(request): reply = convert_to_bq_json( output_type, udf(*convert_call(input_types, call)) ) + if type(reply) is list: + # Since the BQ remote function does not support array yet, + # return a json serialized version of the reply + reply = json.dumps(reply) replies.append(reply) return_json = json.dumps({"replies": replies}) return return_json @@ -191,8 +195,15 @@ def udf_http_row_processor(request): replies = [] for call in calls: reply = convert_to_bq_json(output_type, udf(get_pd_series(call[0]))) - if isinstance(reply, float) and (math.isnan(reply) or math.isinf(reply)): - # json serialization of the special float values (nan, inf, -inf) + if type(reply) is list: + # Since the BQ remote function does not support array yet, + # return a json serialized version of the reply. + # Numpy types are not json serializable, so use their Python + # values instead. + reply = [val.item() if hasattr(val, "item") else val for val in reply] + reply = json.dumps(reply) + elif isinstance(reply, float) and (math.isnan(reply) or math.isinf(reply)): + # Json serialization of the special float values (nan, inf, -inf) # is not in strict compliance of the JSON specification # https://docs.python.org/3/library/json.html#basic-usage. # Let's convert them to a quoted string representation ("NaN", @@ -242,14 +253,7 @@ def generate_cloud_function_main_code( output_type: str, is_row_processor=False, ): - """Get main.py code for the cloud function for the given user defined function. - - Args: - input_types (tuple[str]): - Types of the input arguments in BigQuery SQL data type names. - output_type (str): - Types of the output scalar as a BigQuery SQL data type name. - """ + """Get main.py code for the cloud function for the given user defined function.""" # Pickle the udf with all its dependencies udf_code_file, udf_pickle_file = generate_udf_code(def_, directory) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 9617b5d7a5..d038b8f4c0 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -22,17 +22,17 @@ from google.cloud import bigquery -import bigframes import bigframes.constants as constants import bigframes.formatting_helpers as formatting_helpers from bigframes.ml import sql as ml_sql import bigframes.pandas as bpd +import bigframes.session class BaseBqml: """Base class for BQML functionalities.""" - def __init__(self, session: bigframes.Session): + def __init__(self, session: bigframes.session.Session): self._session = session self._base_sql_generator = ml_sql.BaseSqlGenerator() diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index 413bc89588..c98e18322a 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -22,10 +22,10 @@ import bigframes_vendored.sklearn.decomposition._pca from google.cloud import bigquery -import bigframes from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd +import bigframes.session _BQML_PARAMS_MAPPING = {"svd_solver": "pcaSolver"} @@ -49,7 +49,9 @@ def __init__( self._bqml_model_factory = globals.bqml_model_factory() @classmethod - def _from_bq(cls, session: bigframes.Session, bq_model: bigquery.Model) -> PCA: + def _from_bq( + cls, session: bigframes.session.Session, bq_model: bigquery.Model + ) -> PCA: assert bq_model.model_type == "PCA" kwargs = utils.retrieve_params_from_bq_model( diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py index 11dd639849..2633f13411 100644 --- a/bigframes/ml/ensemble.py +++ b/bigframes/ml/ensemble.py @@ -23,10 +23,10 @@ import bigframes_vendored.xgboost.sklearn from google.cloud import bigquery -import bigframes from bigframes.core import log_adapter +import bigframes.dataframe from bigframes.ml import base, core, globals, utils -import bigframes.pandas as bpd +import bigframes.session _BQML_PARAMS_MAPPING = { "booster": "boosterType", @@ -102,7 +102,7 @@ def __init__( @classmethod def _from_bq( - cls, session: bigframes.Session, bq_model: bigquery.Model + cls, session: bigframes.session.Session, bq_model: bigquery.Model ) -> XGBRegressor: assert bq_model.model_type == "BOOSTED_TREE_REGRESSOR" @@ -169,7 +169,7 @@ def _fit( def predict( self, X: utils.ArrayType, - ) -> bpd.DataFrame: + ) -> bigframes.dataframe.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") (X,) = utils.batch_convert_to_dataframe(X, session=self._bqml_model.session) @@ -261,7 +261,7 @@ def __init__( @classmethod def _from_bq( - cls, session: bigframes.Session, bq_model: bigquery.Model + cls, session: bigframes.session.Session, bq_model: bigquery.Model ) -> XGBClassifier: assert bq_model.model_type == "BOOSTED_TREE_CLASSIFIER" @@ -325,7 +325,7 @@ def _fit( ) return self - def predict(self, X: utils.ArrayType) -> bpd.DataFrame: + def predict(self, X: utils.ArrayType) -> bigframes.dataframe.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") (X,) = utils.batch_convert_to_dataframe(X, session=self._bqml_model.session) @@ -410,7 +410,7 @@ def __init__( @classmethod def _from_bq( - cls, session: bigframes.Session, bq_model: bigquery.Model + cls, session: bigframes.session.Session, bq_model: bigquery.Model ) -> RandomForestRegressor: assert bq_model.model_type == "RANDOM_FOREST_REGRESSOR" @@ -474,7 +474,7 @@ def _fit( def predict( self, X: utils.ArrayType, - ) -> bpd.DataFrame: + ) -> bigframes.dataframe.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") (X,) = utils.batch_convert_to_dataframe(X, session=self._bqml_model.session) @@ -576,7 +576,7 @@ def __init__( @classmethod def _from_bq( - cls, session: bigframes.Session, bq_model: bigquery.Model + cls, session: bigframes.session.Session, bq_model: bigquery.Model ) -> RandomForestClassifier: assert bq_model.model_type == "RANDOM_FOREST_CLASSIFIER" @@ -640,7 +640,7 @@ def _fit( def predict( self, X: utils.ArrayType, - ) -> bpd.DataFrame: + ) -> bigframes.dataframe.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") (X,) = utils.batch_convert_to_dataframe(X, session=self._bqml_model.session) diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 9dc1652912..4e6c5036e7 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -20,10 +20,10 @@ from google.cloud import bigquery -import bigframes from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd +import bigframes.session _BQML_PARAMS_MAPPING = { "horizon": "horizon", @@ -133,7 +133,7 @@ def __init__( @classmethod def _from_bq( - cls, session: bigframes.Session, bq_model: bigquery.Model + cls, session: bigframes.session.Session, bq_model: bigquery.Model ) -> ARIMAPlus: assert bq_model.model_type == "ARIMA_PLUS" diff --git a/bigframes/ml/imported.py b/bigframes/ml/imported.py index c62fb21cd3..93152a6b99 100644 --- a/bigframes/ml/imported.py +++ b/bigframes/ml/imported.py @@ -20,10 +20,10 @@ from google.cloud import bigquery -import bigframes from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd +import bigframes.session @log_adapter.class_logger @@ -41,7 +41,7 @@ def __init__( self, model_path: str, *, - session: Optional[bigframes.Session] = None, + session: Optional[bigframes.session.Session] = None, ): self.session = session or bpd.get_global_session() self.model_path = model_path @@ -56,7 +56,7 @@ def _create_bqml_model(self): @classmethod def _from_bq( - cls, session: bigframes.Session, bq_model: bigquery.Model + cls, session: bigframes.session.Session, bq_model: bigquery.Model ) -> TensorFlowModel: assert bq_model.model_type == "TENSORFLOW" @@ -120,7 +120,7 @@ def __init__( self, model_path: str, *, - session: Optional[bigframes.Session] = None, + session: Optional[bigframes.session.Session] = None, ): self.session = session or bpd.get_global_session() self.model_path = model_path @@ -135,7 +135,7 @@ def _create_bqml_model(self): @classmethod def _from_bq( - cls, session: bigframes.Session, bq_model: bigquery.Model + cls, session: bigframes.session.Session, bq_model: bigquery.Model ) -> ONNXModel: assert bq_model.model_type == "ONNX" @@ -218,7 +218,7 @@ def __init__( *, input: Mapping[str, str] = {}, output: Mapping[str, str] = {}, - session: Optional[bigframes.Session] = None, + session: Optional[bigframes.session.Session] = None, ): self.session = session or bpd.get_global_session() self.model_path = model_path @@ -251,7 +251,7 @@ def _create_bqml_model(self): @classmethod def _from_bq( - cls, session: bigframes.Session, bq_model: bigquery.Model + cls, session: bigframes.session.Session, bq_model: bigquery.Model ) -> XGBoostModel: assert bq_model.model_type == "XGBOOST" diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index 722b72f806..46c5744a42 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -24,10 +24,10 @@ import bigframes_vendored.sklearn.linear_model._logistic from google.cloud import bigquery -import bigframes from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd +import bigframes.session _BQML_PARAMS_MAPPING = { "optimize_strategy": "optimizationStrategy", @@ -87,7 +87,7 @@ def __init__( @classmethod def _from_bq( - cls, session: bigframes.Session, bq_model: bigquery.Model + cls, session: bigframes.session.Session, bq_model: bigquery.Model ) -> LinearRegression: assert bq_model.model_type == "LINEAR_REGRESSION" @@ -282,7 +282,7 @@ def __init__( @classmethod def _from_bq( - cls, session: bigframes.Session, bq_model: bigquery.Model + cls, session: bigframes.session.Session, bq_model: bigquery.Model ) -> LogisticRegression: assert bq_model.model_type == "LOGISTIC_REGRESSION" diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 8d1df6e0b9..bdefc793f9 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -23,12 +23,10 @@ from google.cloud import bigquery import typing_extensions -import bigframes from bigframes import clients, exceptions -from bigframes.core import blocks, log_adapter +from bigframes.core import blocks, global_session, log_adapter import bigframes.dataframe from bigframes.ml import base, core, globals, utils -import bigframes.pandas as bpd _BQML_PARAMS_MAPPING = { "max_iterations": "maxIterations", @@ -145,7 +143,7 @@ def __init__( max_iterations: int = 300, ): self.model_name = model_name - self.session = session or bpd.get_global_session() + self.session = session or global_session.get_global_session() self.max_iterations = max_iterations self._bq_connection_manager = self.session.bqconnectionmanager @@ -275,7 +273,7 @@ def predict( max_output_tokens: int = 128, top_k: int = 40, top_p: float = 0.95, - ) -> bpd.DataFrame: + ) -> bigframes.dataframe.DataFrame: """Predict the result from input DataFrame. Args: @@ -374,7 +372,7 @@ def score( task_type: Literal[ "text_generation", "classification", "summarization", "question_answering" ] = "text_generation", - ) -> bpd.DataFrame: + ) -> bigframes.dataframe.DataFrame: """Calculate evaluation metrics of the model. .. note:: @@ -479,7 +477,7 @@ def __init__( ): self.model_name = model_name self.version = version - self.session = session or bpd.get_global_session() + self.session = session or global_session.get_global_session() self._bq_connection_manager = self.session.bqconnectionmanager connection_name = connection_name or self.session._bq_connection @@ -556,7 +554,7 @@ def _from_bq( model._bqml_model = core.BqmlModel(session, bq_model) return model - def predict(self, X: utils.ArrayType) -> bpd.DataFrame: + def predict(self, X: utils.ArrayType) -> bigframes.dataframe.DataFrame: """Predict the result from input DataFrame. Args: @@ -644,7 +642,7 @@ def __init__( connection_name: Optional[str] = None, ): self.model_name = model_name - self.session = session or bpd.get_global_session() + self.session = session or global_session.get_global_session() self._bq_connection_manager = self.session.bqconnectionmanager connection_name = connection_name or self.session._bq_connection @@ -715,14 +713,20 @@ def _from_bq( return model @property - def _predict_func(self) -> Callable[[bpd.DataFrame, Mapping], bpd.DataFrame]: + def _predict_func( + self, + ) -> Callable[ + [bigframes.dataframe.DataFrame, Mapping], bigframes.dataframe.DataFrame + ]: return self._bqml_model.generate_embedding @property def _status_col(self) -> str: return _ML_GENERATE_EMBEDDING_STATUS - def predict(self, X: utils.ArrayType, *, max_retries: int = 0) -> bpd.DataFrame: + def predict( + self, X: utils.ArrayType, *, max_retries: int = 0 + ) -> bigframes.dataframe.DataFrame: """Predict the result from input DataFrame. Args: @@ -822,7 +826,7 @@ def __init__( ) warnings.warn(msg, category=exceptions.PreviewWarning) self.model_name = model_name - self.session = session or bpd.get_global_session() + self.session = session or global_session.get_global_session() self.max_iterations = max_iterations self._bq_connection_manager = self.session.bqconnectionmanager @@ -899,7 +903,11 @@ def _bqml_options(self) -> dict: return options @property - def _predict_func(self) -> Callable[[bpd.DataFrame, Mapping], bpd.DataFrame]: + def _predict_func( + self, + ) -> Callable[ + [bigframes.dataframe.DataFrame, Mapping], bigframes.dataframe.DataFrame + ]: return self._bqml_model.generate_text @property @@ -962,7 +970,7 @@ def predict( top_p: float = 1.0, ground_with_google_search: bool = False, max_retries: int = 0, - ) -> bpd.DataFrame: + ) -> bigframes.dataframe.DataFrame: """Predict the result from input DataFrame. Args: @@ -1052,7 +1060,7 @@ def score( task_type: Literal[ "text_generation", "classification", "summarization", "question_answering" ] = "text_generation", - ) -> bpd.DataFrame: + ) -> bigframes.dataframe.DataFrame: """Calculate evaluation metrics of the model. Only support "gemini-pro" and "gemini-1.5-pro-002", and "gemini-1.5-flash-002". .. note:: @@ -1170,7 +1178,7 @@ def __init__( connection_name: Optional[str] = None, ): self.model_name = model_name - self.session = session or bpd.get_global_session() + self.session = session or global_session.get_global_session() self._bq_connection_manager = self.session.bqconnectionmanager connection_name = connection_name or self.session._bq_connection @@ -1253,7 +1261,11 @@ def _bqml_options(self) -> dict: return options @property - def _predict_func(self) -> Callable[[bpd.DataFrame, Mapping], bpd.DataFrame]: + def _predict_func( + self, + ) -> Callable[ + [bigframes.dataframe.DataFrame, Mapping], bigframes.dataframe.DataFrame + ]: return self._bqml_model.generate_text @property @@ -1268,7 +1280,7 @@ def predict( top_k: int = 40, top_p: float = 0.95, max_retries: int = 0, - ) -> bpd.DataFrame: + ) -> bigframes.dataframe.DataFrame: """Predict the result from input DataFrame. Args: diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index 1cf8dc8a53..5d52927ded 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -20,7 +20,6 @@ import bigframes_vendored.constants as constants from google.cloud import bigquery -import bigframes from bigframes.ml import ( cluster, compose, @@ -36,6 +35,7 @@ preprocessing, utils, ) +import bigframes.session _BQML_MODEL_TYPE_MAPPING = MappingProxyType( { @@ -80,7 +80,7 @@ def from_bq( - session: bigframes.Session, bq_model: bigquery.Model + session: bigframes.session.Session, bq_model: bigquery.Model ) -> Union[ decomposition.PCA, cluster.KMeans, @@ -121,7 +121,7 @@ def from_bq( return _model_from_bq(session, bq_model) -def _transformer_from_bq(session: bigframes.Session, bq_model: bigquery.Model): +def _transformer_from_bq(session: bigframes.session.Session, bq_model: bigquery.Model): transformer = compose.ColumnTransformer._extract_from_bq_model(bq_model)._merge( bq_model ) @@ -130,7 +130,7 @@ def _transformer_from_bq(session: bigframes.Session, bq_model: bigquery.Model): return transformer -def _model_from_bq(session: bigframes.Session, bq_model: bigquery.Model): +def _model_from_bq(session: bigframes.session.Session, bq_model: bigquery.Model): if bq_model.model_type in _BQML_MODEL_TYPE_MAPPING: return _BQML_MODEL_TYPE_MAPPING[bq_model.model_type]._from_bq( # type: ignore session=session, bq_model=bq_model diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index 5318f0c531..dac51b1956 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -24,8 +24,8 @@ import bigframes_vendored.sklearn.pipeline from google.cloud import bigquery -import bigframes from bigframes.core import log_adapter +import bigframes.dataframe from bigframes.ml import ( base, compose, @@ -35,7 +35,7 @@ preprocessing, utils, ) -import bigframes.pandas as bpd +import bigframes.session @log_adapter.class_logger @@ -92,7 +92,9 @@ def __init__(self, steps: List[Tuple[str, base.BaseEstimator]]): self._estimator = estimator @classmethod - def _from_bq(cls, session: bigframes.Session, bq_model: bigquery.Model) -> Pipeline: + def _from_bq( + cls, session: bigframes.session.Session, bq_model: bigquery.Model + ) -> Pipeline: col_transformer = compose.ColumnTransformer._extract_from_bq_model(bq_model) transform = col_transformer._merge(bq_model) @@ -115,14 +117,14 @@ def fit( self._estimator._fit(X=X, y=y, transforms=transform_sqls) return self - def predict(self, X: utils.ArrayType) -> bpd.DataFrame: + def predict(self, X: utils.ArrayType) -> bigframes.dataframe.DataFrame: return self._estimator.predict(X) def score( self, X: utils.BigFramesArrayType, y: Optional[utils.BigFramesArrayType] = None, - ) -> bpd.DataFrame: + ) -> bigframes.dataframe.DataFrame: (X,) = utils.batch_convert_to_dataframe(X) if y is not None: (y,) = utils.batch_convert_to_dataframe(y) diff --git a/bigframes/ml/remote.py b/bigframes/ml/remote.py index f4f55ad34e..21a3a50421 100644 --- a/bigframes/ml/remote.py +++ b/bigframes/ml/remote.py @@ -19,11 +19,11 @@ from typing import Mapping, Optional import warnings -import bigframes from bigframes import clients -from bigframes.core import log_adapter +from bigframes.core import global_session, log_adapter +import bigframes.dataframe from bigframes.ml import base, core, globals, utils -import bigframes.pandas as bpd +import bigframes.session _REMOTE_MODEL_STATUS = "remote_model_status" @@ -54,13 +54,13 @@ def __init__( input: Mapping[str, str], output: Mapping[str, str], *, - session: Optional[bigframes.Session] = None, + session: Optional[bigframes.session.Session] = None, connection_name: Optional[str] = None, ): self.endpoint = endpoint self.input = input self.output = output - self.session = session or bpd.get_global_session() + self.session = session or global_session.get_global_session() self._bq_connection_manager = self.session.bqconnectionmanager connection_name = connection_name or self.session._bq_connection @@ -122,15 +122,15 @@ def standardize_type(v: str): def predict( self, X: utils.ArrayType, - ) -> bpd.DataFrame: + ) -> bigframes.dataframe.DataFrame: """Predict the result from the input DataFrame. Args: - X (bigframes.dataframe.DataFrame or bigframes.series.Series or pandas.core.frame.DataFrame or pandas.core.series.Series): + X (bigframes.pandas.DataFrame or bigframes.pandas.Series or pandas.DataFrame or pandas.Series): Input DataFrame or Series, which needs to comply with the input parameter of the model. Returns: - bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_input_columns + n_prediction_columns). Returns predicted values. + bigframes.pandas.DataFrame: DataFrame of shape (n_samples, n_input_columns + n_prediction_columns). Returns predicted values. """ (X,) = utils.batch_convert_to_dataframe(X, session=self._bqml_model.session) diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 37a40b7d01..e55cbc4925 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -14,1042 +14,322 @@ from __future__ import annotations -import dataclasses -import datetime -import functools -import typing -from typing import Union - -import numpy as np -import pandas as pd -from pandas.tseries.offsets import DateOffset -import pyarrow as pa - -import bigframes.dtypes as dtypes -import bigframes.operations.type as op_typing - -if typing.TYPE_CHECKING: - # Avoids circular dependency - import bigframes.core.expression - - -class RowOp(typing.Protocol): - @property - def name(self) -> str: - ... - - def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: - ... - - @property - def order_preserving(self) -> bool: - """Whether the row operation preserves total ordering. Can be pruned from ordering expressions.""" - ... - - -@dataclasses.dataclass(frozen=True) -class ScalarOp: - @property - def name(self) -> str: - raise NotImplementedError("RowOp abstract base class has no implementation") - - def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: - raise NotImplementedError("Abstract operation has no output type") - - @property - def order_preserving(self) -> bool: - """Whether the row operation preserves total ordering. Can be pruned from ordering expressions.""" - return False - - -@dataclasses.dataclass(frozen=True) -class NaryOp(ScalarOp): - def as_expr( - self, - *exprs: Union[str | bigframes.core.expression.Expression], - ) -> bigframes.core.expression.Expression: - import bigframes.core.expression - - # Keep this in sync with output_type and compilers - inputs: list[bigframes.core.expression.Expression] = [] - - for expr in exprs: - inputs.append(_convert_expr_input(expr)) - - return bigframes.core.expression.OpExpression( - self, - tuple(inputs), - ) - - -# These classes can be used to create simple ops that don't take local parameters -# All is needed is a unique name, and to register an implementation in ibis_mappings.py -@dataclasses.dataclass(frozen=True) -class UnaryOp(ScalarOp): - @property - def arguments(self) -> int: - return 1 - - def as_expr( - self, input_id: typing.Union[str, bigframes.core.expression.Expression] = "arg" - ) -> bigframes.core.expression.Expression: - import bigframes.core.expression - - return bigframes.core.expression.OpExpression( - self, (_convert_expr_input(input_id),) - ) - - -@dataclasses.dataclass(frozen=True) -class BinaryOp(ScalarOp): - @property - def arguments(self) -> int: - return 2 - - def as_expr( - self, - left_input: typing.Union[str, bigframes.core.expression.Expression] = "arg1", - right_input: typing.Union[str, bigframes.core.expression.Expression] = "arg2", - ) -> bigframes.core.expression.Expression: - import bigframes.core.expression - - return bigframes.core.expression.OpExpression( - self, - ( - _convert_expr_input(left_input), - _convert_expr_input(right_input), - ), - ) - - -@dataclasses.dataclass(frozen=True) -class TernaryOp(ScalarOp): - @property - def arguments(self) -> int: - return 3 - - def as_expr( - self, - input1: typing.Union[str, bigframes.core.expression.Expression] = "arg1", - input2: typing.Union[str, bigframes.core.expression.Expression] = "arg2", - input3: typing.Union[str, bigframes.core.expression.Expression] = "arg3", - ) -> bigframes.core.expression.Expression: - import bigframes.core.expression - - return bigframes.core.expression.OpExpression( - self, - ( - _convert_expr_input(input1), - _convert_expr_input(input2), - _convert_expr_input(input3), - ), - ) - - -def _convert_expr_input( - input: typing.Union[str, bigframes.core.expression.Expression] -) -> bigframes.core.expression.Expression: - """Allows creating column references with just a string""" - import bigframes.core.expression - - if isinstance(input, str): - return bigframes.core.expression.deref(input) - else: - return input - - -# Operation Factories -def create_unary_op(name: str, type_signature: op_typing.UnaryTypeSignature) -> UnaryOp: - return dataclasses.make_dataclass( - name, - [ - ("name", typing.ClassVar[str], name), - ("output_type", typing.ClassVar[typing.Callable], type_signature.as_method), - ], - bases=(UnaryOp,), - frozen=True, - )() - - -def create_binary_op( - name: str, type_signature: op_typing.BinaryTypeSignature -) -> BinaryOp: - return dataclasses.make_dataclass( - name, - [ - ("name", typing.ClassVar[str], name), - ("output_type", typing.ClassVar[typing.Callable], type_signature.as_method), - ], - bases=(BinaryOp,), - frozen=True, - )() - - -# Unary Ops -## Generic Ops -invert_op = create_unary_op( - name="invert", - type_signature=op_typing.TypePreserving( - dtypes.is_binary_like, - description="binary-like", - ), -) -isnull_op = create_unary_op( - name="isnull", - type_signature=op_typing.FixedOutputType( - lambda x: True, dtypes.BOOL_DTYPE, description="nullable" - ), -) -notnull_op = create_unary_op( - name="notnull", - type_signature=op_typing.FixedOutputType( - lambda x: True, dtypes.BOOL_DTYPE, description="nullable" - ), -) -hash_op = create_unary_op( - name="hash", - type_signature=op_typing.FixedOutputType( - dtypes.is_string_like, dtypes.INT_DTYPE, description="string-like" - ), -) -## String Ops -len_op = create_unary_op( - name="len", - type_signature=op_typing.FixedOutputType( - dtypes.is_iterable, dtypes.INT_DTYPE, description="iterable" - ), -) -reverse_op = create_unary_op(name="reverse", type_signature=op_typing.STRING_TRANSFORM) -lower_op = create_unary_op(name="lower", type_signature=op_typing.STRING_TRANSFORM) -upper_op = create_unary_op(name="upper", type_signature=op_typing.STRING_TRANSFORM) -strip_op = create_unary_op(name="strip", type_signature=op_typing.STRING_TRANSFORM) -isalnum_op = create_unary_op(name="isalnum", type_signature=op_typing.STRING_PREDICATE) -isalpha_op = create_unary_op(name="isalpha", type_signature=op_typing.STRING_PREDICATE) -isdecimal_op = create_unary_op( - name="isdecimal", type_signature=op_typing.STRING_PREDICATE +from bigframes.operations.array_ops import ArrayIndexOp, ArraySliceOp, ArrayToStringOp +from bigframes.operations.base_ops import ( + BinaryOp, + NaryOp, + RowOp, + ScalarOp, + TernaryOp, + UnaryOp, ) -isdigit_op = create_unary_op(name="isdigit", type_signature=op_typing.STRING_PREDICATE) -isnumeric_op = create_unary_op( - name="isnumeric", type_signature=op_typing.STRING_PREDICATE +from bigframes.operations.blob_ops import ( + obj_fetch_metadata_op, + obj_make_ref_op, + ObjGetAccessUrl, ) -isspace_op = create_unary_op(name="isspace", type_signature=op_typing.STRING_PREDICATE) -islower_op = create_unary_op(name="islower", type_signature=op_typing.STRING_PREDICATE) -isupper_op = create_unary_op(name="isupper", type_signature=op_typing.STRING_PREDICATE) -rstrip_op = create_unary_op(name="rstrip", type_signature=op_typing.STRING_TRANSFORM) -lstrip_op = create_unary_op(name="lstrip", type_signature=op_typing.STRING_TRANSFORM) -capitalize_op = create_unary_op( - name="capitalize", type_signature=op_typing.STRING_TRANSFORM +from bigframes.operations.bool_ops import and_op, or_op, xor_op +from bigframes.operations.comparison_ops import ( + eq_null_match_op, + eq_op, + ge_op, + gt_op, + le_op, + lt_op, + ne_op, ) -## DateTime Ops -### datelike accessors -day_op = create_unary_op( - name="day", - type_signature=op_typing.DATELIKE_ACCESSOR, +from bigframes.operations.date_ops import ( + day_op, + dayofweek_op, + month_op, + quarter_op, + year_op, ) -month_op = create_unary_op( - name="month", - type_signature=op_typing.DATELIKE_ACCESSOR, +from bigframes.operations.datetime_ops import ( + date_op, + StrftimeOp, + time_op, + ToDatetimeOp, + ToTimestampOp, + UnixMicros, + UnixMillis, + UnixSeconds, ) -year_op = create_unary_op( - name="year", - type_signature=op_typing.DATELIKE_ACCESSOR, +from bigframes.operations.distance_ops import ( + cosine_distance_op, + euclidean_distance_op, + manhattan_distance_op, ) -dayofweek_op = create_unary_op( - name="dayofweek", - type_signature=op_typing.DATELIKE_ACCESSOR, +from bigframes.operations.frequency_ops import ( + DatetimeToIntegerLabelOp, + FloorDtOp, + IntegerLabelToDatetimeOp, ) -quarter_op = create_unary_op( - name="quarter", - type_signature=op_typing.DATELIKE_ACCESSOR, +from bigframes.operations.generic_ops import ( + AsTypeOp, + case_when_op, + CaseWhenOp, + clip_op, + coalesce_op, + fillna_op, + hash_op, + invert_op, + IsInOp, + isnull_op, + MapOp, + maximum_op, + minimum_op, + notnull_op, + RowKey, + SqlScalarOp, + where_op, ) -### timelike accessors -hour_op = create_unary_op( - name="hour", - type_signature=op_typing.TIMELIKE_ACCESSOR, +from bigframes.operations.geo_ops import geo_x_op, geo_y_op +from bigframes.operations.json_ops import ( + JSONExtract, + JSONExtractArray, + JSONExtractStringArray, + JSONSet, + JSONValue, + ParseJSON, + ToJSONString, ) -minute_op = create_unary_op( - name="minute", - type_signature=op_typing.TIMELIKE_ACCESSOR, +from bigframes.operations.numeric_ops import ( + abs_op, + add_op, + arccos_op, + arccosh_op, + arcsin_op, + arcsinh_op, + arctan2_op, + arctan_op, + arctanh_op, + ceil_op, + cos_op, + cosh_op, + div_op, + exp_op, + expm1_op, + floor_op, + floordiv_op, + ln_op, + log1p_op, + log10_op, + mod_op, + mul_op, + neg_op, + pos_op, + pow_op, + round_op, + sin_op, + sinh_op, + sqrt_op, + sub_op, + tan_op, + tanh_op, + unsafe_pow_op, ) -second_op = create_unary_op( - name="second", - type_signature=op_typing.TIMELIKE_ACCESSOR, +from bigframes.operations.numpy_op_maps import NUMPY_TO_BINOP, NUMPY_TO_OP +from bigframes.operations.remote_function_ops import ( + BinaryRemoteFunctionOp, + NaryRemoteFunctionOp, + RemoteFunctionOp, ) -normalize_op = create_unary_op( - name="normalize", - type_signature=op_typing.TypePreserving( - dtypes.is_time_like, - description="time-like", - ), +from bigframes.operations.string_ops import ( + capitalize_op, + EndsWithOp, + isalnum_op, + isalpha_op, + isdecimal_op, + isdigit_op, + islower_op, + isnumeric_op, + isspace_op, + isupper_op, + len_op, + lower_op, + lstrip_op, + RegexReplaceStrOp, + ReplaceStrOp, + reverse_op, + rstrip_op, + StartsWithOp, + strconcat_op, + StrContainsOp, + StrContainsRegexOp, + StrExtractOp, + StrFindOp, + StrGetOp, + StringSplitOp, + strip_op, + StrPadOp, + StrRepeatOp, + StrSliceOp, + upper_op, + ZfillOp, ) -### datetimelike accessors -date_op = create_unary_op( - name="date", - type_signature=op_typing.FixedOutputType( - dtypes.is_date_like, dtypes.DATE_DTYPE, description="date-like" - ), -) -time_op = create_unary_op( - name="time", - type_signature=op_typing.FixedOutputType( - dtypes.is_time_like, dtypes.TIME_DTYPE, description="time-like" - ), -) -## Trigonometry Ops -sin_op = create_unary_op(name="sin", type_signature=op_typing.UNARY_REAL_NUMERIC) -cos_op = create_unary_op(name="cos", type_signature=op_typing.UNARY_REAL_NUMERIC) -tan_op = create_unary_op(name="tan", type_signature=op_typing.UNARY_REAL_NUMERIC) -arcsin_op = create_unary_op(name="arcsin", type_signature=op_typing.UNARY_REAL_NUMERIC) -arccos_op = create_unary_op(name="arccos", type_signature=op_typing.UNARY_REAL_NUMERIC) -arctan_op = create_unary_op(name="arctan", type_signature=op_typing.UNARY_REAL_NUMERIC) -sinh_op = create_unary_op(name="sinh", type_signature=op_typing.UNARY_REAL_NUMERIC) -cosh_op = create_unary_op(name="cosh", type_signature=op_typing.UNARY_REAL_NUMERIC) -tanh_op = create_unary_op(name="tanh", type_signature=op_typing.UNARY_REAL_NUMERIC) -arcsinh_op = create_unary_op( - name="arcsinh", type_signature=op_typing.UNARY_REAL_NUMERIC -) -arccosh_op = create_unary_op( - name="arccosh", type_signature=op_typing.UNARY_REAL_NUMERIC -) -arctanh_op = create_unary_op( - name="arctanh", type_signature=op_typing.UNARY_REAL_NUMERIC -) -# Geo Ops -geo_x_op = create_unary_op( - name="geo_x", - type_signature=op_typing.FixedOutputType( - dtypes.is_geo_like, dtypes.FLOAT_DTYPE, description="geo-like" - ), -) -geo_y_op = create_unary_op( - name="geo_y", - type_signature=op_typing.FixedOutputType( - dtypes.is_geo_like, dtypes.FLOAT_DTYPE, description="geo-like" - ), -) -## Numeric Ops -floor_op = create_unary_op(name="floor", type_signature=op_typing.UNARY_REAL_NUMERIC) -ceil_op = create_unary_op(name="ceil", type_signature=op_typing.UNARY_REAL_NUMERIC) -abs_op = create_unary_op(name="abs", type_signature=op_typing.UNARY_NUMERIC) -pos_op = create_unary_op(name="pos", type_signature=op_typing.UNARY_NUMERIC) -neg_op = create_unary_op(name="neg", type_signature=op_typing.UNARY_NUMERIC) -exp_op = create_unary_op(name="exp", type_signature=op_typing.UNARY_REAL_NUMERIC) -expm1_op = create_unary_op(name="expm1", type_signature=op_typing.UNARY_REAL_NUMERIC) -ln_op = create_unary_op(name="log", type_signature=op_typing.UNARY_REAL_NUMERIC) -log10_op = create_unary_op(name="log10", type_signature=op_typing.UNARY_REAL_NUMERIC) -log1p_op = create_unary_op(name="log1p", type_signature=op_typing.UNARY_REAL_NUMERIC) -sqrt_op = create_unary_op(name="sqrt", type_signature=op_typing.UNARY_REAL_NUMERIC) -## Blob Ops -obj_fetch_metadata_op = create_unary_op( - name="obj_fetch_metadata", type_signature=op_typing.BLOB_TRANSFORM -) - - -# Parameterized unary ops -@dataclasses.dataclass(frozen=True) -class StrContainsOp(UnaryOp): - name: typing.ClassVar[str] = "str_contains" - pat: str - - def output_type(self, *input_types): - return op_typing.STRING_PREDICATE.output_type(input_types[0]) - - -@dataclasses.dataclass(frozen=True) -class StrContainsRegexOp(UnaryOp): - name: typing.ClassVar[str] = "str_contains_regex" - pat: str - - def output_type(self, *input_types): - return op_typing.STRING_PREDICATE.output_type(input_types[0]) - - -@dataclasses.dataclass(frozen=True) -class StrGetOp(UnaryOp): - name: typing.ClassVar[str] = "str_get" - i: int - - def output_type(self, *input_types): - return op_typing.STRING_TRANSFORM.output_type(input_types[0]) - - -@dataclasses.dataclass(frozen=True) -class StrPadOp(UnaryOp): - name: typing.ClassVar[str] = "str_pad" - length: int - fillchar: str - side: typing.Literal["both", "left", "right"] - - def output_type(self, *input_types): - return op_typing.STRING_TRANSFORM.output_type(input_types[0]) - - -@dataclasses.dataclass(frozen=True) -class ReplaceStrOp(UnaryOp): - name: typing.ClassVar[str] = "str_replace" - pat: str - repl: str - - def output_type(self, *input_types): - return op_typing.STRING_TRANSFORM.output_type(input_types[0]) - - -@dataclasses.dataclass(frozen=True) -class RegexReplaceStrOp(UnaryOp): - name: typing.ClassVar[str] = "str_rereplace" - pat: str - repl: str - - def output_type(self, *input_types): - return op_typing.STRING_TRANSFORM.output_type(input_types[0]) - - -@dataclasses.dataclass(frozen=True) -class StartsWithOp(UnaryOp): - name: typing.ClassVar[str] = "str_startswith" - pat: typing.Sequence[str] - - def output_type(self, *input_types): - return op_typing.STRING_PREDICATE.output_type(input_types[0]) - - -@dataclasses.dataclass(frozen=True) -class StringSplitOp(UnaryOp): - name: typing.ClassVar[str] = "str_split" - pat: typing.Sequence[str] - - def output_type(self, *input_types): - input_type = input_types[0] - if not isinstance(input_type, pd.StringDtype): - raise TypeError("field accessor input must be a string type") - arrow_type = dtypes.bigframes_dtype_to_arrow_dtype(input_type) - return pd.ArrowDtype(pa.list_(arrow_type)) - - -@dataclasses.dataclass(frozen=True) -class EndsWithOp(UnaryOp): - name: typing.ClassVar[str] = "str_endswith" - pat: typing.Sequence[str] - - def output_type(self, *input_types): - return op_typing.STRING_PREDICATE.output_type(input_types[0]) - - -@dataclasses.dataclass(frozen=True) -class ZfillOp(UnaryOp): - name: typing.ClassVar[str] = "str_zfill" - width: int - - def output_type(self, *input_types): - return op_typing.STRING_TRANSFORM.output_type(input_types[0]) - - -@dataclasses.dataclass(frozen=True) -class StrFindOp(UnaryOp): - name: typing.ClassVar[str] = "str_find" - substr: str - start: typing.Optional[int] - end: typing.Optional[int] - - def output_type(self, *input_types): - signature = op_typing.FixedOutputType( - dtypes.is_string_like, dtypes.INT_DTYPE, "string-like" - ) - return signature.output_type(input_types[0]) - - -@dataclasses.dataclass(frozen=True) -class StrExtractOp(UnaryOp): - name: typing.ClassVar[str] = "str_extract" - pat: str - n: int = 1 - - def output_type(self, *input_types): - return op_typing.STRING_TRANSFORM.output_type(input_types[0]) - - -@dataclasses.dataclass(frozen=True) -class StrSliceOp(UnaryOp): - name: typing.ClassVar[str] = "str_slice" - start: typing.Optional[int] - end: typing.Optional[int] - - def output_type(self, *input_types): - return op_typing.STRING_TRANSFORM.output_type(input_types[0]) - - -@dataclasses.dataclass(frozen=True) -class StrRepeatOp(UnaryOp): - name: typing.ClassVar[str] = "str_repeat" - repeats: int - - def output_type(self, *input_types): - return op_typing.STRING_TRANSFORM.output_type(input_types[0]) - - -# Other parameterized unary operations -@dataclasses.dataclass(frozen=True) -class StructFieldOp(UnaryOp): - name: typing.ClassVar[str] = "struct_field" - name_or_index: str | int - - def output_type(self, *input_types): - input_type = input_types[0] - if not isinstance(input_type, pd.ArrowDtype): - raise TypeError("field accessor input must be a struct type") - - pa_type = input_type.pyarrow_dtype - if not isinstance(pa_type, pa.StructType): - raise TypeError("field accessor input must be a struct type") - - pa_result_type = pa_type[self.name_or_index].type - return dtypes.arrow_dtype_to_bigframes_dtype(pa_result_type) - - -@dataclasses.dataclass(frozen=True) -class AsTypeOp(UnaryOp): - name: typing.ClassVar[str] = "astype" - # TODO: Convert strings to dtype earlier - to_type: dtypes.DtypeString | dtypes.Dtype - safe: bool = False - - def output_type(self, *input_types): - # TODO: We should do this conversion earlier - if self.to_type == pa.string(): - return dtypes.STRING_DTYPE - if isinstance(self.to_type, str): - return dtypes.BIGFRAMES_STRING_TO_BIGFRAMES[ - typing.cast(dtypes.DtypeString, self.to_type) - ] - return self.to_type - - -@dataclasses.dataclass(frozen=True) -class IsInOp(UnaryOp): - name: typing.ClassVar[str] = "is_in" - values: typing.Tuple - match_nulls: bool = True - - def output_type(self, *input_types): - return dtypes.BOOL_DTYPE - - -@dataclasses.dataclass(frozen=True) -class RemoteFunctionOp(UnaryOp): - name: typing.ClassVar[str] = "remote_function" - func: typing.Callable - apply_on_null: bool - - def output_type(self, *input_types): - # This property should be set to a valid Dtype by the @remote_function decorator or read_gbq_function method - if hasattr(self.func, "output_dtype"): - if dtypes.is_array_like(self.func.output_dtype): - # TODO(b/284515241): remove this special handling to support - # array output types once BQ remote functions support ARRAY. - # Until then, use json serialized strings at the remote function - # level, and parse that to the intended output type at the - # bigframes level. - return dtypes.STRING_DTYPE - return self.func.output_dtype - else: - raise AttributeError("output_dtype not defined") - - -@dataclasses.dataclass(frozen=True) -class MapOp(UnaryOp): - name = "map_values" - mappings: typing.Tuple[typing.Tuple[typing.Hashable, typing.Hashable], ...] - - def output_type(self, *input_types): - return input_types[0] - - -@dataclasses.dataclass(frozen=True) -class ToDatetimeOp(UnaryOp): - name: typing.ClassVar[str] = "to_datetime" - format: typing.Optional[str] = None - unit: typing.Optional[str] = None - - def output_type(self, *input_types): - if input_types[0] not in ( - dtypes.FLOAT_DTYPE, - dtypes.INT_DTYPE, - dtypes.STRING_DTYPE, - ): - raise TypeError("expected string or numeric input") - return pd.ArrowDtype(pa.timestamp("us", tz=None)) - - -@dataclasses.dataclass(frozen=True) -class ToTimestampOp(UnaryOp): - name: typing.ClassVar[str] = "to_timestamp" - format: typing.Optional[str] = None - unit: typing.Optional[str] = None - - def output_type(self, *input_types): - # Must be numeric or string - if input_types[0] not in ( - dtypes.FLOAT_DTYPE, - dtypes.INT_DTYPE, - dtypes.STRING_DTYPE, - ): - raise TypeError("expected string or numeric input") - return pd.ArrowDtype(pa.timestamp("us", tz="UTC")) - - -@dataclasses.dataclass(frozen=True) -class StrftimeOp(UnaryOp): - name: typing.ClassVar[str] = "strftime" - date_format: str - - def output_type(self, *input_types): - return dtypes.STRING_DTYPE - - -@dataclasses.dataclass(frozen=True) -class FloorDtOp(UnaryOp): - name: typing.ClassVar[str] = "floor_dt" - freq: str - - def output_type(self, *input_types): - return input_types[0] - - -@dataclasses.dataclass(frozen=True) -class DatetimeToIntegerLabelOp(BinaryOp): - name: typing.ClassVar[str] = "datetime_to_integer_label" - freq: DateOffset - closed: typing.Optional[typing.Literal["right", "left"]] - origin: Union[ - Union[pd.Timestamp, datetime.datetime, np.datetime64, int, float, str], - typing.Literal["epoch", "start", "start_day", "end", "end_day"], - ] - - def output_type(self, *input_types): - return dtypes.INT_DTYPE - - -@dataclasses.dataclass(frozen=True) -class IntegerLabelToDatetimeOp(BinaryOp): - name: typing.ClassVar[str] = "integer_label_to_datetime" - freq: DateOffset - label: typing.Optional[typing.Literal["right", "left"]] - origin: Union[ - Union[pd.Timestamp, datetime.datetime, np.datetime64, int, float, str], - typing.Literal["epoch", "start", "start_day", "end", "end_day"], - ] - - def output_type(self, *input_types): - return input_types[1] - - -## Array Ops -@dataclasses.dataclass(frozen=True) -class ArrayToStringOp(UnaryOp): - name: typing.ClassVar[str] = "array_to_string" - delimiter: str - - def output_type(self, *input_types): - input_type = input_types[0] - if not dtypes.is_array_string_like(input_type): - raise TypeError("Input type must be an array of string type.") - return dtypes.STRING_DTYPE - - -@dataclasses.dataclass(frozen=True) -class ArrayIndexOp(UnaryOp): - name: typing.ClassVar[str] = "array_index" - index: int - - def output_type(self, *input_types): - input_type = input_types[0] - if dtypes.is_string_like(input_type): - return dtypes.STRING_DTYPE - elif dtypes.is_array_like(input_type): - return dtypes.arrow_dtype_to_bigframes_dtype( - input_type.pyarrow_dtype.value_type - ) - else: - raise TypeError("Input type must be an array or string-like type.") - - -@dataclasses.dataclass(frozen=True) -class ArraySliceOp(UnaryOp): - name: typing.ClassVar[str] = "array_slice" - start: int - stop: typing.Optional[int] = None - step: typing.Optional[int] = None - - def output_type(self, *input_types): - input_type = input_types[0] - if dtypes.is_string_like(input_type): - return dtypes.STRING_DTYPE - elif dtypes.is_array_like(input_type): - return input_type - else: - raise TypeError("Input type must be an array or string-like type.") - - -## JSON Ops -@dataclasses.dataclass(frozen=True) -class JSONExtract(UnaryOp): - name: typing.ClassVar[str] = "json_extract" - json_path: str - - def output_type(self, *input_types): - input_type = input_types[0] - if not dtypes.is_json_like(input_type): - raise TypeError( - "Input type must be an valid JSON object or JSON-formatted string type." - + f" Received type: {input_type}" - ) - return input_type - - -@dataclasses.dataclass(frozen=True) -class JSONExtractArray(UnaryOp): - name: typing.ClassVar[str] = "json_extract_array" - json_path: str - - def output_type(self, *input_types): - input_type = input_types[0] - if not dtypes.is_json_like(input_type): - raise TypeError( - "Input type must be an valid JSON object or JSON-formatted string type." - + f" Received type: {input_type}" - ) - return pd.ArrowDtype( - pa.list_(dtypes.bigframes_dtype_to_arrow_dtype(dtypes.STRING_DTYPE)) - ) - - -@dataclasses.dataclass(frozen=True) -class JSONExtractStringArray(UnaryOp): - name: typing.ClassVar[str] = "json_extract_string_array" - json_path: str - - def output_type(self, *input_types): - input_type = input_types[0] - if not dtypes.is_json_like(input_type): - raise TypeError( - "Input type must be an valid JSON object or JSON-formatted string type." - + f" Received type: {input_type}" - ) - return pd.ArrowDtype( - pa.list_(dtypes.bigframes_dtype_to_arrow_dtype(dtypes.STRING_DTYPE)) - ) - - -@dataclasses.dataclass(frozen=True) -class ParseJSON(UnaryOp): - name: typing.ClassVar[str] = "parse_json" - - def output_type(self, *input_types): - input_type = input_types[0] - if input_type != dtypes.STRING_DTYPE: - raise TypeError( - "Input type must be an valid JSON-formatted string type." - + f" Received type: {input_type}" - ) - return dtypes.JSON_DTYPE - - -@dataclasses.dataclass(frozen=True) -class ToJSONString(UnaryOp): - name: typing.ClassVar[str] = "to_json_string" - - def output_type(self, *input_types): - input_type = input_types[0] - if not dtypes.is_json_like(input_type): - raise TypeError( - "Input type must be an valid JSON object or JSON-formatted string type." - + f" Received type: {input_type}" - ) - return dtypes.STRING_DTYPE - - -## Blob Ops -@dataclasses.dataclass(frozen=True) -class ObjGetAccessUrl(UnaryOp): - name: typing.ClassVar[str] = "obj_get_access_url" - mode: str # access mode, e.g. R read, W write, RW read & write - - def output_type(self, *input_types): - return dtypes.JSON_DTYPE - - -# Binary Ops -fillna_op = create_binary_op(name="fillna", type_signature=op_typing.COERCE) -maximum_op = create_binary_op(name="maximum", type_signature=op_typing.COERCE) -minimum_op = create_binary_op(name="minimum", type_signature=op_typing.COERCE) -coalesce_op = create_binary_op(name="coalesce", type_signature=op_typing.COERCE) - - -## Math Ops -@dataclasses.dataclass(frozen=True) -class AddOp(BinaryOp): - name: typing.ClassVar[str] = "add" - - def output_type(self, *input_types): - left_type = input_types[0] - right_type = input_types[1] - if all(map(dtypes.is_string_like, input_types)) and len(set(input_types)) == 1: - # String addition - return input_types[0] - if (left_type is None or dtypes.is_numeric(left_type)) and ( - right_type is None or dtypes.is_numeric(right_type) - ): - # Numeric addition - return dtypes.coerce_to_common(left_type, right_type) - # TODO: Add temporal addition once delta types supported - raise TypeError(f"Cannot add dtypes {left_type} and {right_type}") - - -@dataclasses.dataclass(frozen=True) -class SubOp(BinaryOp): - name: typing.ClassVar[str] = "sub" - - # Note: this is actualyl a vararg op, but we don't model that yet - def output_type(self, *input_types): - left_type = input_types[0] - right_type = input_types[1] - if (left_type is None or dtypes.is_numeric(left_type)) and ( - right_type is None or dtypes.is_numeric(right_type) - ): - # Numeric subtraction - return dtypes.coerce_to_common(left_type, right_type) - # TODO: Add temporal addition once delta types supported - raise TypeError(f"Cannot subtract dtypes {left_type} and {right_type}") - - -@dataclasses.dataclass(frozen=True) -class BinaryRemoteFunctionOp(BinaryOp): - name: typing.ClassVar[str] = "binary_remote_function" - func: typing.Callable - - def output_type(self, *input_types): - # This property should be set to a valid Dtype by the @remote_function decorator or read_gbq_function method - if hasattr(self.func, "output_dtype"): - return self.func.output_dtype - else: - raise AttributeError("output_dtype not defined") - - -@dataclasses.dataclass(frozen=True) -class NaryRemoteFunctionOp(NaryOp): - name: typing.ClassVar[str] = "nary_remote_function" - func: typing.Callable - - def output_type(self, *input_types): - # This property should be set to a valid Dtype by the @remote_function decorator or read_gbq_function method - if hasattr(self.func, "output_dtype"): - return self.func.output_dtype - else: - raise AttributeError("output_dtype not defined") - - -add_op = AddOp() -sub_op = SubOp() -mul_op = create_binary_op(name="mul", type_signature=op_typing.BINARY_NUMERIC) -div_op = create_binary_op(name="div", type_signature=op_typing.BINARY_REAL_NUMERIC) -floordiv_op = create_binary_op(name="floordiv", type_signature=op_typing.BINARY_NUMERIC) -pow_op = create_binary_op(name="pow", type_signature=op_typing.BINARY_NUMERIC) -mod_op = create_binary_op(name="mod", type_signature=op_typing.BINARY_NUMERIC) -arctan2_op = create_binary_op( - name="arctan2", type_signature=op_typing.BINARY_REAL_NUMERIC -) -round_op = create_binary_op(name="round", type_signature=op_typing.BINARY_REAL_NUMERIC) -unsafe_pow_op = create_binary_op( - name="unsafe_pow_op", type_signature=op_typing.BINARY_REAL_NUMERIC -) -# Logical Ops -and_op = create_binary_op(name="and", type_signature=op_typing.LOGICAL) -or_op = create_binary_op(name="or", type_signature=op_typing.LOGICAL) -xor_op = create_binary_op(name="xor", type_signature=op_typing.LOGICAL) - -## Comparison Ops -eq_op = create_binary_op(name="eq", type_signature=op_typing.COMPARISON) -eq_null_match_op = create_binary_op( - name="eq_nulls_match", type_signature=op_typing.COMPARISON -) -ne_op = create_binary_op(name="ne", type_signature=op_typing.COMPARISON) -lt_op = create_binary_op(name="lt", type_signature=op_typing.COMPARISON) -gt_op = create_binary_op(name="gt", type_signature=op_typing.COMPARISON) -le_op = create_binary_op(name="le", type_signature=op_typing.COMPARISON) -ge_op = create_binary_op(name="ge", type_signature=op_typing.COMPARISON) - - -cosine_distance_op = create_binary_op( - name="ml_cosine_distance", type_signature=op_typing.VECTOR_METRIC -) -manhattan_distance_op = create_binary_op( - name="ml_manhattan_distance", type_signature=op_typing.VECTOR_METRIC -) -euclidean_distance_op = create_binary_op( - name="ml_euclidean_distance", type_signature=op_typing.VECTOR_METRIC -) - - -## String Ops -@dataclasses.dataclass(frozen=True) -class StrConcatOp(BinaryOp): - name: typing.ClassVar[str] = "str_concat" - - # Note: this is actualyl a vararg op, but we don't model that yet - def output_type(self, *input_types): - if not all(map(dtypes.is_string_like, input_types)): - raise TypeError("string concat requires string-like arguments") - if len(set(input_types)) != 1: - raise TypeError("string concat requires like-typed arguments") - return input_types[0] - - -strconcat_op = StrConcatOp() - - -## JSON Ops -@dataclasses.dataclass(frozen=True) -class JSONSet(BinaryOp): - name: typing.ClassVar[str] = "json_set" - json_path: str - - def output_type(self, *input_types): - left_type = input_types[0] - right_type = input_types[1] - if not dtypes.is_json_like(left_type): - raise TypeError( - "Input type must be an valid JSON object or JSON-formatted string type." - + f" Received type: {left_type}" - ) - if not dtypes.is_json_encoding_type(right_type): - raise TypeError( - "The value to be assigned must be a type that can be encoded as JSON." - + f"Received type: {right_type}" - ) - - # After JSON type implementation, ONLY return JSON data. - return left_type - - -## Blob Ops -@dataclasses.dataclass(frozen=True) -class ObjMakeRef(BinaryOp): - name: typing.ClassVar[str] = "obj.make_ref" - - def output_type(self, *input_types): - if not all(map(dtypes.is_string_like, input_types)): - raise TypeError("obj.make_ref requires string-like arguments") - - return dtypes.OBJ_REF_DTYPE - - -obj_make_ref_op = ObjMakeRef() - - -# Ternary Ops -@dataclasses.dataclass(frozen=True) -class WhereOp(TernaryOp): - name: typing.ClassVar[str] = "where" - - def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: - if input_types[1] != dtypes.BOOL_DTYPE: - raise TypeError("where condition must be a boolean") - return dtypes.coerce_to_common(input_types[0], input_types[2]) - - -where_op = WhereOp() - - -@dataclasses.dataclass(frozen=True) -class ClipOp(TernaryOp): - name: typing.ClassVar[str] = "clip" - - def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: - return dtypes.coerce_to_common( - input_types[0], dtypes.coerce_to_common(input_types[1], input_types[2]) - ) - - -clip_op = ClipOp() - - -class CaseWhenOp(NaryOp): - name: typing.ClassVar[str] = "switch" - - def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: - assert len(input_types) % 2 == 0 - # predicate1, output1, predicate2, output2... - if not all(map(lambda x: x == dtypes.BOOL_DTYPE, input_types[::2])): - raise TypeError(f"Case inputs {input_types[::2]} must be boolean-valued") - output_expr_types = input_types[1::2] - return functools.reduce( - lambda t1, t2: dtypes.coerce_to_common(t1, t2), - output_expr_types, - ) - - -case_when_op = CaseWhenOp() - - -@dataclasses.dataclass(frozen=True) -class StructOp(NaryOp): - name: typing.ClassVar[str] = "struct" - column_names: tuple[str] - - def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: - num_input_types = len(input_types) - # value1, value2, ... - assert num_input_types == len(self.column_names) - fields = [] - - for i in range(num_input_types): - arrow_type = dtypes.bigframes_dtype_to_arrow_dtype(input_types[i]) - fields.append( - pa.field( - self.column_names[i], - arrow_type, - nullable=(not pa.types.is_list(arrow_type)), - ) - ) - return pd.ArrowDtype( - pa.struct(fields) - ) # [(name1, value1), (name2, value2), ...] - - -# Just parameterless unary ops for now -# TODO: Parameter mappings -NUMPY_TO_OP: dict[np.ufunc, UnaryOp] = { - np.sin: sin_op, - np.cos: cos_op, - np.tan: tan_op, - np.arcsin: arcsin_op, - np.arccos: arccos_op, - np.arctan: arctan_op, - np.sinh: sinh_op, - np.cosh: cosh_op, - np.tanh: tanh_op, - np.arcsinh: arcsinh_op, - np.arccosh: arccosh_op, - np.arctanh: arctanh_op, - np.exp: exp_op, - np.log: ln_op, - np.log10: log10_op, - np.sqrt: sqrt_op, - np.abs: abs_op, - np.floor: floor_op, - np.ceil: ceil_op, - np.log1p: log1p_op, - np.expm1: expm1_op, -} - - -NUMPY_TO_BINOP: dict[np.ufunc, BinaryOp] = { - np.add: add_op, - np.subtract: sub_op, - np.multiply: mul_op, - np.divide: div_op, - np.power: pow_op, - np.arctan2: arctan2_op, - np.maximum: maximum_op, - np.minimum: minimum_op, -} +from bigframes.operations.struct_ops import StructFieldOp, StructOp +from bigframes.operations.time_ops import hour_op, minute_op, normalize_op, second_op + +__all__ = [ + # Base ops + "RowOp", + "NaryOp", + "UnaryOp", + "BinaryOp", + "TernaryOp", + "ScalarOp", + # Generic ops + "AsTypeOp", + "case_when_op", + "CaseWhenOp", + "clip_op", + "coalesce_op", + "fillna_op", + "hash_op", + "invert_op", + "IsInOp", + "isnull_op", + "MapOp", + "maximum_op", + "minimum_op", + "notnull_op", + "RowKey", + "SqlScalarOp", + "where_op", + # String ops + "capitalize_op", + "EndsWithOp", + "isalnum_op", + "isalpha_op", + "isdecimal_op", + "isdigit_op", + "islower_op", + "isnumeric_op", + "isspace_op", + "isupper_op", + "len_op", + "lower_op", + "lstrip_op", + "RegexReplaceStrOp", + "ReplaceStrOp", + "reverse_op", + "rstrip_op", + "StartsWithOp", + "strconcat_op", + "StrContainsOp", + "StrContainsRegexOp", + "StrExtractOp", + "StrFindOp", + "StrGetOp", + "StringSplitOp", + "strip_op", + "StrPadOp", + "StrRepeatOp", + "StrSliceOp", + "upper_op", + "ZfillOp", + # Date ops + "day_op", + "month_op", + "year_op", + "dayofweek_op", + "quarter_op", + # Time ops + "hour_op", + "minute_op", + "second_op", + "normalize_op", + # Datetime ops + "date_op", + "time_op", + "ToDatetimeOp", + "ToTimestampOp", + "StrftimeOp", + "UnixMicros", + "UnixMillis", + "UnixSeconds", + # Numeric ops + "abs_op", + "add_op", + "arccos_op", + "arccosh_op", + "arcsin_op", + "arcsinh_op", + "arctan2_op", + "arctan_op", + "arctanh_op", + "ceil_op", + "cos_op", + "cosh_op", + "div_op", + "exp_op", + "expm1_op", + "floor_op", + "floordiv_op", + "ln_op", + "log1p_op", + "log10_op", + "mod_op", + "mul_op", + "neg_op", + "pos_op", + "pow_op", + "round_op", + "sin_op", + "sinh_op", + "sqrt_op", + "sub_op", + "tan_op", + "tanh_op", + "unsafe_pow_op", + # Array ops + "ArrayIndexOp", + "ArraySliceOp", + "ArrayToStringOp", + # Blob ops + "ObjGetAccessUrl", + "obj_make_ref_op", + "obj_fetch_metadata_op", + # Struct ops + "StructFieldOp", + "StructOp", + # Remote Functions ops + "BinaryRemoteFunctionOp", + "NaryRemoteFunctionOp", + "RemoteFunctionOp", + # Frequency ops + "DatetimeToIntegerLabelOp", + "FloorDtOp", + "IntegerLabelToDatetimeOp", + # JSON ops + "JSONExtract", + "JSONExtractArray", + "JSONExtractStringArray", + "JSONSet", + "JSONValue", + "ParseJSON", + "ToJSONString", + # Bool ops + "and_op", + "or_op", + "xor_op", + # Comparison ops + "eq_null_match_op", + "eq_op", + "ge_op", + "gt_op", + "le_op", + "lt_op", + "ne_op", + # Distance ops + "cosine_distance_op", + "euclidean_distance_op", + "manhattan_distance_op", + # Geo ops + "geo_x_op", + "geo_y_op", + # Numpy ops mapping + "NUMPY_TO_BINOP", + "NUMPY_TO_OP", +] diff --git a/bigframes/operations/array_ops.py b/bigframes/operations/array_ops.py new file mode 100644 index 0000000000..c1e644fc11 --- /dev/null +++ b/bigframes/operations/array_ops.py @@ -0,0 +1,65 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import dataclasses +import typing + +from bigframes import dtypes +from bigframes.operations import base_ops + + +@dataclasses.dataclass(frozen=True) +class ArrayToStringOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "array_to_string" + delimiter: str + + def output_type(self, *input_types): + input_type = input_types[0] + if not dtypes.is_array_string_like(input_type): + raise TypeError("Input type must be an array of string type.") + return dtypes.STRING_DTYPE + + +@dataclasses.dataclass(frozen=True) +class ArrayIndexOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "array_index" + index: int + + def output_type(self, *input_types): + input_type = input_types[0] + if dtypes.is_string_like(input_type): + return dtypes.STRING_DTYPE + elif dtypes.is_array_like(input_type): + return dtypes.arrow_dtype_to_bigframes_dtype( + input_type.pyarrow_dtype.value_type + ) + else: + raise TypeError("Input type must be an array or string-like type.") + + +@dataclasses.dataclass(frozen=True) +class ArraySliceOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "array_slice" + start: int + stop: typing.Optional[int] = None + step: typing.Optional[int] = None + + def output_type(self, *input_types): + input_type = input_types[0] + if dtypes.is_string_like(input_type): + return dtypes.STRING_DTYPE + elif dtypes.is_array_like(input_type): + return input_type + else: + raise TypeError("Input type must be an array or string-like type.") diff --git a/bigframes/operations/base_ops.py b/bigframes/operations/base_ops.py new file mode 100644 index 0000000000..0308283ad4 --- /dev/null +++ b/bigframes/operations/base_ops.py @@ -0,0 +1,197 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import dataclasses +import typing + +from bigframes import dtypes +import bigframes.operations.type as op_typing + +if typing.TYPE_CHECKING: + # Avoids circular dependency + import bigframes.core.expression + + +class RowOp(typing.Protocol): + @property + def name(self) -> str: + ... + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + ... + + @property + def is_monotonic(self) -> bool: + """Whether the row operation preserves total ordering. Can be pruned from ordering expressions.""" + ... + + @property + def is_bijective(self) -> bool: + """Whether the operation has a 1:1 mapping between inputs and outputs""" + ... + + @property + def deterministic(self) -> bool: + """Whether the operation is deterministic" (given deterministic inputs)""" + ... + + +@dataclasses.dataclass(frozen=True) +class ScalarOp: + @property + def name(self) -> str: + raise NotImplementedError("RowOp abstract base class has no implementation") + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + raise NotImplementedError("Abstract operation has no output type") + + @property + def is_monotonic(self) -> bool: + """Whether the row operation preserves total ordering. Can be pruned from ordering expressions.""" + return False + + @property + def is_bijective(self) -> bool: + """Whether the operation has a 1:1 mapping between inputs and outputs""" + return False + + @property + def deterministic(self) -> bool: + """Whether the operation is deterministic" (given deterministic inputs)""" + return True + + +@dataclasses.dataclass(frozen=True) +class NaryOp(ScalarOp): + def as_expr( + self, + *exprs: typing.Union[str, bigframes.core.expression.Expression], + ) -> bigframes.core.expression.Expression: + import bigframes.core.expression + + # Keep this in sync with output_type and compilers + inputs: list[bigframes.core.expression.Expression] = [] + + for expr in exprs: + inputs.append(_convert_expr_input(expr)) + + return bigframes.core.expression.OpExpression( + self, + tuple(inputs), + ) + + +# These classes can be used to create simple ops that don't take local parameters +# All is needed is a unique name, and to register an implementation in ibis_mappings.py +@dataclasses.dataclass(frozen=True) +class UnaryOp(ScalarOp): + @property + def arguments(self) -> int: + return 1 + + def as_expr( + self, input_id: typing.Union[str, bigframes.core.expression.Expression] = "arg" + ) -> bigframes.core.expression.Expression: + import bigframes.core.expression + + return bigframes.core.expression.OpExpression( + self, (_convert_expr_input(input_id),) + ) + + +@dataclasses.dataclass(frozen=True) +class BinaryOp(ScalarOp): + @property + def arguments(self) -> int: + return 2 + + def as_expr( + self, + left_input: typing.Union[str, bigframes.core.expression.Expression] = "arg1", + right_input: typing.Union[str, bigframes.core.expression.Expression] = "arg2", + ) -> bigframes.core.expression.Expression: + import bigframes.core.expression + + return bigframes.core.expression.OpExpression( + self, + ( + _convert_expr_input(left_input), + _convert_expr_input(right_input), + ), + ) + + +@dataclasses.dataclass(frozen=True) +class TernaryOp(ScalarOp): + @property + def arguments(self) -> int: + return 3 + + def as_expr( + self, + input1: typing.Union[str, bigframes.core.expression.Expression] = "arg1", + input2: typing.Union[str, bigframes.core.expression.Expression] = "arg2", + input3: typing.Union[str, bigframes.core.expression.Expression] = "arg3", + ) -> bigframes.core.expression.Expression: + import bigframes.core.expression + + return bigframes.core.expression.OpExpression( + self, + ( + _convert_expr_input(input1), + _convert_expr_input(input2), + _convert_expr_input(input3), + ), + ) + + +def _convert_expr_input( + input: typing.Union[str, bigframes.core.expression.Expression] +) -> bigframes.core.expression.Expression: + """Allows creating column references with just a string""" + import bigframes.core.expression + + if isinstance(input, str): + return bigframes.core.expression.deref(input) + else: + return input + + +# Operation Factories +def create_unary_op(name: str, type_signature: op_typing.UnaryTypeSignature) -> UnaryOp: + return dataclasses.make_dataclass( + name, + [ + ("name", typing.ClassVar[str], name), + ("output_type", typing.ClassVar[typing.Callable], type_signature.as_method), + ], + bases=(UnaryOp,), + frozen=True, + )() + + +def create_binary_op( + name: str, type_signature: op_typing.BinaryTypeSignature +) -> BinaryOp: + return dataclasses.make_dataclass( + name, + [ + ("name", typing.ClassVar[str], name), + ("output_type", typing.ClassVar[typing.Callable], type_signature.as_method), + ], + bases=(BinaryOp,), + frozen=True, + )() diff --git a/bigframes/operations/blob.py b/bigframes/operations/blob.py index f78db2b6fc..a4de2f80c7 100644 --- a/bigframes/operations/blob.py +++ b/bigframes/operations/blob.py @@ -34,22 +34,159 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + def uri(self) -> bigframes.series.Series: + """URIs of the Blob. + + .. note:: + BigFrames Blob is still under experiments. It may not work and subject to change in the future. + + Returns: + BigFrames Series: URIs as string.""" + s = bigframes.series.Series(self._block) + + return s.struct.field("uri") + + def authorizer(self) -> bigframes.series.Series: + """Authorizers of the Blob. + + .. note:: + BigFrames Blob is still under experiments. It may not work and subject to change in the future. + + Returns: + BigFrames Series: Autorithers(connection) as string.""" + s = bigframes.series.Series(self._block) + + return s.struct.field("authorizer") + + def version(self) -> bigframes.series.Series: + """Versions of the Blob. + + .. note:: + BigFrames Blob is still under experiments. It may not work and subject to change in the future. + + Returns: + BigFrames Series: Version as string.""" + # version must be retrieved after fetching metadata + return self._apply_unary_op(ops.obj_fetch_metadata_op).struct.field("version") + def metadata(self) -> bigframes.series.Series: - """Retrive the metadata of the Blob. + """Retrieve the metadata of the Blob. .. note:: BigFrames Blob is still under experiments. It may not work and subject to change in the future. Returns: - JSON: metadata of the Blob. Contains fields: content_type, md5_hash, size and updated(time).""" + BigFrames Series: JSON metadata of the Blob. Contains fields: content_type, md5_hash, size and updated(time).""" details_json = self._apply_unary_op(ops.obj_fetch_metadata_op).struct.field( "details" ) import bigframes.bigquery as bbq - return bbq.json_extract(details_json, "$.gcs_metadata") + return bbq.json_extract(details_json, "$.gcs_metadata").rename("metadata") - def display(self, n: int = 3): + def content_type(self) -> bigframes.series.Series: + """Retrieve the content type of the Blob. + + .. note:: + BigFrames Blob is still under experiments. It may not work and subject to change in the future. + + Returns: + BigFrames Series: string of the content type.""" + return ( + self.metadata() + ._apply_unary_op(ops.JSONValue(json_path="$.content_type")) + .rename("content_type") + ) + + def md5_hash(self) -> bigframes.series.Series: + """Retrieve the md5 hash of the Blob. + + .. note:: + BigFrames Blob is still under experiments. It may not work and subject to change in the future. + + Returns: + BigFrames Series: string of the md5 hash.""" + return ( + self.metadata() + ._apply_unary_op(ops.JSONValue(json_path="$.md5_hash")) + .rename("md5_hash") + ) + + def size(self) -> bigframes.series.Series: + """Retrieve the file size of the Blob. + + .. note:: + BigFrames Blob is still under experiments. It may not work and subject to change in the future. + + Returns: + BigFrames Series: file size in bytes.""" + return ( + self.metadata() + ._apply_unary_op(ops.JSONValue(json_path="$.size")) + .rename("size") + .astype("Int64") + ) + + def updated(self) -> bigframes.series.Series: + """Retrieve the updated time of the Blob. + + .. note:: + BigFrames Blob is still under experiments. It may not work and subject to change in the future. + + Returns: + BigFrames Series: updated time as UTC datetime.""" + import bigframes.pandas as bpd + + updated = ( + self.metadata() + ._apply_unary_op(ops.JSONValue(json_path="$.updated")) + .rename("updated") + .astype("Int64") + ) + + return bpd.to_datetime(updated, unit="us", utc=True) + + def _get_runtime( + self, mode: str, with_metadata: bool = False + ) -> bigframes.series.Series: + """Retrieve the ObjectRefRuntime as JSON. + + Args: + mode (str): mode for the URLs, "R" for read, "RW" for read & write. + metadata (bool, default False): whether to fetch the metadata in the ObjectRefRuntime. + + Returns: + bigframes Series: ObjectRefRuntime JSON. + """ + s = self._apply_unary_op(ops.obj_fetch_metadata_op) if with_metadata else self + + return s._apply_unary_op(ops.ObjGetAccessUrl(mode=mode)) + + def read_url(self) -> bigframes.series.Series: + """Retrieve the read URL of the Blob. + + .. note:: + BigFrames Blob is still under experiments. It may not work and subject to change in the future. + + Returns: + BigFrames Series: Read only URLs.""" + return self._get_runtime(mode="R")._apply_unary_op( + ops.JSONValue(json_path="$.access_urls.read_url") + ) + + def write_url(self) -> bigframes.series.Series: + """Retrieve the write URL of the Blob. + + .. note:: + BigFrames Blob is still under experiments. It may not work and subject to change in the future. + + Returns: + BigFrames Series: Writable URLs.""" + return self._get_runtime(mode="RW")._apply_unary_op( + ops.JSONValue(json_path="$.access_urls.write_url") + ) + + def display(self, n: int = 3, *, content_type: str = ""): """Display the blob content in the IPython Notebook environment. Only works for image type now. .. note:: @@ -57,20 +194,35 @@ def display(self, n: int = 3): Args: n (int, default 3): number of sample blob objects to display. + content_type (str, default ""): content type of the blob. If unset, use the blob metadata of the storage. Possible values are "image", "audio" and "video". """ - import bigframes.bigquery as bbq + # col name doesn't matter here. Rename to avoid column name conflicts + df = bigframes.series.Series(self._block).rename("blob_col").head(n).to_frame() - s = bigframes.series.Series(self._block).head(n) + df["read_url"] = df["blob_col"].blob.read_url() - obj_ref_runtime = s._apply_unary_op(ops.ObjGetAccessUrl(mode="R")) - read_urls = bbq.json_extract( - obj_ref_runtime, json_path="$.access_urls.read_url" - ) + if content_type: + df["content_type"] = content_type + else: + df["content_type"] = df["blob_col"].blob.content_type() + + def display_single_url(read_url: str, content_type: str): + content_type = content_type.casefold() - for read_url in read_urls: - read_url = str(read_url).strip('"') - response = requests.get(read_url) - ipy_display.display(ipy_display.Image(response.content)) + if content_type.startswith("image"): + ipy_display.display(ipy_display.Image(url=read_url)) + elif content_type.startswith("audio"): + # using url somehow doesn't work with audios + response = requests.get(read_url) + ipy_display.display(ipy_display.Audio(response.content)) + elif content_type.startswith("video"): + ipy_display.display(ipy_display.Video(url=read_url)) + else: # display as raw data + response = requests.get(read_url) + ipy_display.display(response.content) + + for _, row in df.iterrows(): + display_single_url(row["read_url"], row["content_type"]) def image_blur( self, @@ -116,10 +268,8 @@ def image_blur( connection=connection, ).udf() - src_rt = bigframes.series.Series(self._block)._apply_unary_op( - ops.ObjGetAccessUrl(mode="R") - ) - dst_rt = dst._apply_unary_op(ops.ObjGetAccessUrl(mode="RW")) + src_rt = self._get_runtime(mode="R") + dst_rt = dst.blob._get_runtime(mode="RW") src_rt = src_rt._apply_unary_op(ops.ToJSONString()) dst_rt = dst_rt._apply_unary_op(ops.ToJSONString()) diff --git a/bigframes/operations/blob_ops.py b/bigframes/operations/blob_ops.py new file mode 100644 index 0000000000..b17d1b1215 --- /dev/null +++ b/bigframes/operations/blob_ops.py @@ -0,0 +1,47 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import dataclasses +import typing + +from bigframes import dtypes +from bigframes.operations import base_ops +import bigframes.operations.type as op_typing + +obj_fetch_metadata_op = base_ops.create_unary_op( + name="obj_fetch_metadata", type_signature=op_typing.BLOB_TRANSFORM +) + + +@dataclasses.dataclass(frozen=True) +class ObjGetAccessUrl(base_ops.UnaryOp): + name: typing.ClassVar[str] = "obj_get_access_url" + mode: str # access mode, e.g. R read, W write, RW read & write + + def output_type(self, *input_types): + return dtypes.JSON_DTYPE + + +@dataclasses.dataclass(frozen=True) +class ObjMakeRef(base_ops.BinaryOp): + name: typing.ClassVar[str] = "obj.make_ref" + + def output_type(self, *input_types): + if not all(map(dtypes.is_string_like, input_types)): + raise TypeError("obj.make_ref requires string-like arguments") + + return dtypes.OBJ_REF_DTYPE + + +obj_make_ref_op = ObjMakeRef() diff --git a/bigframes/operations/bool_ops.py b/bigframes/operations/bool_ops.py new file mode 100644 index 0000000000..c8cd08efe5 --- /dev/null +++ b/bigframes/operations/bool_ops.py @@ -0,0 +1,23 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from bigframes.operations import base_ops +import bigframes.operations.type as op_typing + +and_op = base_ops.create_binary_op(name="and", type_signature=op_typing.LOGICAL) + +or_op = base_ops.create_binary_op(name="or", type_signature=op_typing.LOGICAL) + +xor_op = base_ops.create_binary_op(name="xor", type_signature=op_typing.LOGICAL) diff --git a/bigframes/operations/comparison_ops.py b/bigframes/operations/comparison_ops.py new file mode 100644 index 0000000000..b109a85d18 --- /dev/null +++ b/bigframes/operations/comparison_ops.py @@ -0,0 +1,33 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from bigframes.operations import base_ops +import bigframes.operations.type as op_typing + +eq_op = base_ops.create_binary_op(name="eq", type_signature=op_typing.COMPARISON) + +eq_null_match_op = base_ops.create_binary_op( + name="eq_nulls_match", type_signature=op_typing.COMPARISON +) + +ne_op = base_ops.create_binary_op(name="ne", type_signature=op_typing.COMPARISON) + +lt_op = base_ops.create_binary_op(name="lt", type_signature=op_typing.COMPARISON) + +gt_op = base_ops.create_binary_op(name="gt", type_signature=op_typing.COMPARISON) + +le_op = base_ops.create_binary_op(name="le", type_signature=op_typing.COMPARISON) + +ge_op = base_ops.create_binary_op(name="ge", type_signature=op_typing.COMPARISON) diff --git a/bigframes/operations/date_ops.py b/bigframes/operations/date_ops.py new file mode 100644 index 0000000000..2b68a24caf --- /dev/null +++ b/bigframes/operations/date_ops.py @@ -0,0 +1,41 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from bigframes.operations import base_ops +import bigframes.operations.type as op_typing + +day_op = base_ops.create_unary_op( + name="day", + type_signature=op_typing.DATELIKE_ACCESSOR, +) + +month_op = base_ops.create_unary_op( + name="month", + type_signature=op_typing.DATELIKE_ACCESSOR, +) + +year_op = base_ops.create_unary_op( + name="year", + type_signature=op_typing.DATELIKE_ACCESSOR, +) + +dayofweek_op = base_ops.create_unary_op( + name="dayofweek", + type_signature=op_typing.DATELIKE_ACCESSOR, +) + +quarter_op = base_ops.create_unary_op( + name="quarter", + type_signature=op_typing.DATELIKE_ACCESSOR, +) diff --git a/bigframes/operations/datetime_ops.py b/bigframes/operations/datetime_ops.py new file mode 100644 index 0000000000..3ee8a00141 --- /dev/null +++ b/bigframes/operations/datetime_ops.py @@ -0,0 +1,103 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import dataclasses +import typing + +import pandas as pd +import pyarrow as pa + +from bigframes import dtypes +from bigframes.operations import base_ops +import bigframes.operations.type as op_typing + +date_op = base_ops.create_unary_op( + name="date", + type_signature=op_typing.FixedOutputType( + dtypes.is_date_like, dtypes.DATE_DTYPE, description="date-like" + ), +) + +time_op = base_ops.create_unary_op( + name="time", + type_signature=op_typing.FixedOutputType( + dtypes.is_time_like, dtypes.TIME_DTYPE, description="time-like" + ), +) + + +@dataclasses.dataclass(frozen=True) +class ToDatetimeOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "to_datetime" + format: typing.Optional[str] = None + unit: typing.Optional[str] = None + + def output_type(self, *input_types): + if input_types[0] not in ( + dtypes.FLOAT_DTYPE, + dtypes.INT_DTYPE, + dtypes.STRING_DTYPE, + ): + raise TypeError("expected string or numeric input") + return pd.ArrowDtype(pa.timestamp("us", tz=None)) + + +@dataclasses.dataclass(frozen=True) +class ToTimestampOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "to_timestamp" + format: typing.Optional[str] = None + unit: typing.Optional[str] = None + + def output_type(self, *input_types): + # Must be numeric or string + if input_types[0] not in ( + dtypes.FLOAT_DTYPE, + dtypes.INT_DTYPE, + dtypes.STRING_DTYPE, + ): + raise TypeError("expected string or numeric input") + return pd.ArrowDtype(pa.timestamp("us", tz="UTC")) + + +@dataclasses.dataclass(frozen=True) +class StrftimeOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "strftime" + date_format: str + + def output_type(self, *input_types): + return dtypes.STRING_DTYPE + + +@dataclasses.dataclass(frozen=True) +class UnixSeconds(base_ops.UnaryOp): + name: typing.ClassVar[str] = "unix_seconds" + + def output_type(self, *input_types): + return dtypes.INT_DTYPE + + +@dataclasses.dataclass(frozen=True) +class UnixMillis(base_ops.UnaryOp): + name: typing.ClassVar[str] = "unix_millis" + + def output_type(self, *input_types): + return dtypes.INT_DTYPE + + +@dataclasses.dataclass(frozen=True) +class UnixMicros(base_ops.UnaryOp): + name: typing.ClassVar[str] = "unix_micros" + + def output_type(self, *input_types): + return dtypes.INT_DTYPE diff --git a/bigframes/operations/distance_ops.py b/bigframes/operations/distance_ops.py new file mode 100644 index 0000000000..74595b561a --- /dev/null +++ b/bigframes/operations/distance_ops.py @@ -0,0 +1,29 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from bigframes.operations import base_ops +import bigframes.operations.type as op_typing + +cosine_distance_op = base_ops.create_binary_op( + name="ml_cosine_distance", type_signature=op_typing.VECTOR_METRIC +) + +manhattan_distance_op = base_ops.create_binary_op( + name="ml_manhattan_distance", type_signature=op_typing.VECTOR_METRIC +) + +euclidean_distance_op = base_ops.create_binary_op( + name="ml_euclidean_distance", type_signature=op_typing.VECTOR_METRIC +) diff --git a/bigframes/operations/frequency_ops.py b/bigframes/operations/frequency_ops.py new file mode 100644 index 0000000000..2d5a854c32 --- /dev/null +++ b/bigframes/operations/frequency_ops.py @@ -0,0 +1,61 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import dataclasses +import datetime +import typing + +import numpy as np +import pandas as pd +from pandas.tseries import offsets + +from bigframes import dtypes +from bigframes.operations import base_ops + + +@dataclasses.dataclass(frozen=True) +class FloorDtOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "floor_dt" + freq: str + + def output_type(self, *input_types): + return input_types[0] + + +@dataclasses.dataclass(frozen=True) +class DatetimeToIntegerLabelOp(base_ops.BinaryOp): + name: typing.ClassVar[str] = "datetime_to_integer_label" + freq: offsets.DateOffset + closed: typing.Optional[typing.Literal["right", "left"]] + origin: typing.Union[ + typing.Union[pd.Timestamp, datetime.datetime, np.datetime64, int, float, str], + typing.Literal["epoch", "start", "start_day", "end", "end_day"], + ] + + def output_type(self, *input_types): + return dtypes.INT_DTYPE + + +@dataclasses.dataclass(frozen=True) +class IntegerLabelToDatetimeOp(base_ops.BinaryOp): + name: typing.ClassVar[str] = "integer_label_to_datetime" + freq: offsets.DateOffset + label: typing.Optional[typing.Literal["right", "left"]] + origin: typing.Union[ + typing.Union[pd.Timestamp, datetime.datetime, np.datetime64, int, float, str], + typing.Literal["epoch", "start", "start_day", "end", "end_day"], + ] + + def output_type(self, *input_types): + return input_types[1] diff --git a/bigframes/operations/generic_ops.py b/bigframes/operations/generic_ops.py new file mode 100644 index 0000000000..ef7e1f5cea --- /dev/null +++ b/bigframes/operations/generic_ops.py @@ -0,0 +1,174 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import dataclasses +import functools +import typing + +import pyarrow as pa + +from bigframes import dtypes +from bigframes.operations import base_ops +import bigframes.operations.type as op_typing + +invert_op = base_ops.create_unary_op( + name="invert", + type_signature=op_typing.TypePreserving( + dtypes.is_binary_like, + description="binary-like", + ), +) + +isnull_op = base_ops.create_unary_op( + name="isnull", + type_signature=op_typing.FixedOutputType( + lambda x: True, dtypes.BOOL_DTYPE, description="nullable" + ), +) + +notnull_op = base_ops.create_unary_op( + name="notnull", + type_signature=op_typing.FixedOutputType( + lambda x: True, dtypes.BOOL_DTYPE, description="nullable" + ), +) + +hash_op = base_ops.create_unary_op( + name="hash", + type_signature=op_typing.FixedOutputType( + dtypes.is_string_like, dtypes.INT_DTYPE, description="string-like" + ), +) + + +@dataclasses.dataclass(frozen=True) +class AsTypeOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "astype" + # TODO: Convert strings to dtype earlier + to_type: typing.Union[dtypes.DtypeString, dtypes.Dtype] + safe: bool = False + + def output_type(self, *input_types): + # TODO: We should do this conversion earlier + if self.to_type == pa.string(): + return dtypes.STRING_DTYPE + if isinstance(self.to_type, str): + return dtypes.BIGFRAMES_STRING_TO_BIGFRAMES[ + typing.cast(dtypes.DtypeString, self.to_type) + ] + return self.to_type + + +@dataclasses.dataclass(frozen=True) +class IsInOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "is_in" + values: typing.Tuple + match_nulls: bool = True + + def output_type(self, *input_types): + return dtypes.BOOL_DTYPE + + +@dataclasses.dataclass(frozen=True) +class MapOp(base_ops.UnaryOp): + name = "map_values" + mappings: typing.Tuple[typing.Tuple[typing.Hashable, typing.Hashable], ...] + + def output_type(self, *input_types): + return input_types[0] + + +fillna_op = base_ops.create_binary_op(name="fillna", type_signature=op_typing.COERCE) + +maximum_op = base_ops.create_binary_op(name="maximum", type_signature=op_typing.COERCE) + +minimum_op = base_ops.create_binary_op(name="minimum", type_signature=op_typing.COERCE) + +coalesce_op = base_ops.create_binary_op( + name="coalesce", type_signature=op_typing.COERCE +) + + +@dataclasses.dataclass(frozen=True) +class WhereOp(base_ops.TernaryOp): + name: typing.ClassVar[str] = "where" + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + if input_types[1] != dtypes.BOOL_DTYPE: + raise TypeError("where condition must be a boolean") + return dtypes.coerce_to_common(input_types[0], input_types[2]) + + +where_op = WhereOp() + + +@dataclasses.dataclass(frozen=True) +class ClipOp(base_ops.TernaryOp): + name: typing.ClassVar[str] = "clip" + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return dtypes.coerce_to_common( + input_types[0], dtypes.coerce_to_common(input_types[1], input_types[2]) + ) + + +clip_op = ClipOp() + + +class CaseWhenOp(base_ops.NaryOp): + name: typing.ClassVar[str] = "switch" + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + assert len(input_types) % 2 == 0 + # predicate1, output1, predicate2, output2... + if not all(map(lambda x: x == dtypes.BOOL_DTYPE, input_types[::2])): + raise TypeError(f"Case inputs {input_types[::2]} must be boolean-valued") + output_expr_types = input_types[1::2] + return functools.reduce( + lambda t1, t2: dtypes.coerce_to_common(t1, t2), + output_expr_types, + ) + + +case_when_op = CaseWhenOp() + + +# Really doesn't need to be its own op, but allows us to try to get the most compact representation +@dataclasses.dataclass(frozen=True) +class RowKey(base_ops.NaryOp): + name: typing.ClassVar[str] = "rowkey" + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return dtypes.STRING_DTYPE + + @property + def is_bijective(self) -> bool: + """Whether the operation has a 1:1 mapping between inputs and outputs""" + return True + + @property + def deterministic(self) -> bool: + return False + + +@dataclasses.dataclass(frozen=True) +class SqlScalarOp(base_ops.NaryOp): + """An escape to SQL, representing a single column.""" + + name: typing.ClassVar[str] = "sql_scalar" + _output_type: dtypes.ExpressionType + sql_template: str + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return self._output_type diff --git a/bigframes/operations/geo_ops.py b/bigframes/operations/geo_ops.py new file mode 100644 index 0000000000..73e7e89197 --- /dev/null +++ b/bigframes/operations/geo_ops.py @@ -0,0 +1,31 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from bigframes import dtypes +from bigframes.operations import base_ops +import bigframes.operations.type as op_typing + +geo_x_op = base_ops.create_unary_op( + name="geo_x", + type_signature=op_typing.FixedOutputType( + dtypes.is_geo_like, dtypes.FLOAT_DTYPE, description="geo-like" + ), +) + +geo_y_op = base_ops.create_unary_op( + name="geo_y", + type_signature=op_typing.FixedOutputType( + dtypes.is_geo_like, dtypes.FLOAT_DTYPE, description="geo-like" + ), +) diff --git a/bigframes/operations/json_ops.py b/bigframes/operations/json_ops.py new file mode 100644 index 0000000000..86c5a19ba7 --- /dev/null +++ b/bigframes/operations/json_ops.py @@ -0,0 +1,137 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import dataclasses +import typing + +import pandas as pd +import pyarrow as pa + +from bigframes import dtypes +from bigframes.operations import base_ops + + +@dataclasses.dataclass(frozen=True) +class JSONExtract(base_ops.UnaryOp): + name: typing.ClassVar[str] = "json_extract" + json_path: str + + def output_type(self, *input_types): + input_type = input_types[0] + if not dtypes.is_json_like(input_type): + raise TypeError( + "Input type must be an valid JSON object or JSON-formatted string type." + + f" Received type: {input_type}" + ) + return input_type + + +@dataclasses.dataclass(frozen=True) +class JSONExtractArray(base_ops.UnaryOp): + name: typing.ClassVar[str] = "json_extract_array" + json_path: str + + def output_type(self, *input_types): + input_type = input_types[0] + if not dtypes.is_json_like(input_type): + raise TypeError( + "Input type must be an valid JSON object or JSON-formatted string type." + + f" Received type: {input_type}" + ) + return pd.ArrowDtype( + pa.list_(dtypes.bigframes_dtype_to_arrow_dtype(dtypes.STRING_DTYPE)) + ) + + +@dataclasses.dataclass(frozen=True) +class JSONExtractStringArray(base_ops.UnaryOp): + name: typing.ClassVar[str] = "json_extract_string_array" + json_path: str + + def output_type(self, *input_types): + input_type = input_types[0] + if not dtypes.is_json_like(input_type): + raise TypeError( + "Input type must be an valid JSON object or JSON-formatted string type." + + f" Received type: {input_type}" + ) + return pd.ArrowDtype( + pa.list_(dtypes.bigframes_dtype_to_arrow_dtype(dtypes.STRING_DTYPE)) + ) + + +@dataclasses.dataclass(frozen=True) +class ParseJSON(base_ops.UnaryOp): + name: typing.ClassVar[str] = "parse_json" + + def output_type(self, *input_types): + input_type = input_types[0] + if input_type != dtypes.STRING_DTYPE: + raise TypeError( + "Input type must be an valid JSON-formatted string type." + + f" Received type: {input_type}" + ) + return dtypes.JSON_DTYPE + + +@dataclasses.dataclass(frozen=True) +class ToJSONString(base_ops.UnaryOp): + name: typing.ClassVar[str] = "to_json_string" + + def output_type(self, *input_types): + input_type = input_types[0] + if not dtypes.is_json_like(input_type): + raise TypeError( + "Input type must be an valid JSON object or JSON-formatted string type." + + f" Received type: {input_type}" + ) + return dtypes.STRING_DTYPE + + +@dataclasses.dataclass(frozen=True) +class JSONSet(base_ops.BinaryOp): + name: typing.ClassVar[str] = "json_set" + json_path: str + + def output_type(self, *input_types): + left_type = input_types[0] + right_type = input_types[1] + if not dtypes.is_json_like(left_type): + raise TypeError( + "Input type must be an valid JSON object or JSON-formatted string type." + + f" Received type: {left_type}" + ) + if not dtypes.is_json_encoding_type(right_type): + raise TypeError( + "The value to be assigned must be a type that can be encoded as JSON." + + f"Received type: {right_type}" + ) + + # After JSON type implementation, ONLY return JSON data. + return left_type + + +@dataclasses.dataclass(frozen=True) +class JSONValue(base_ops.UnaryOp): + name: typing.ClassVar[str] = "json_value" + json_path: str + + def output_type(self, *input_types): + input_type = input_types[0] + if not dtypes.is_json_like(input_type): + raise TypeError( + "Input type must be an valid JSON object or JSON-formatted string type." + + f" Received type: {input_type}" + ) + return dtypes.STRING_DTYPE diff --git a/bigframes/operations/numeric_ops.py b/bigframes/operations/numeric_ops.py new file mode 100644 index 0000000000..939330954d --- /dev/null +++ b/bigframes/operations/numeric_ops.py @@ -0,0 +1,174 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import dataclasses +import typing + +from bigframes import dtypes +from bigframes.operations import base_ops +import bigframes.operations.type as op_typing + +sin_op = base_ops.create_unary_op( + name="sin", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +cos_op = base_ops.create_unary_op( + name="cos", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +tan_op = base_ops.create_unary_op( + name="tan", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +arcsin_op = base_ops.create_unary_op( + name="arcsin", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +arccos_op = base_ops.create_unary_op( + name="arccos", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +arctan_op = base_ops.create_unary_op( + name="arctan", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +sinh_op = base_ops.create_unary_op( + name="sinh", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +cosh_op = base_ops.create_unary_op( + name="cosh", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +tanh_op = base_ops.create_unary_op( + name="tanh", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +arcsinh_op = base_ops.create_unary_op( + name="arcsinh", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +arccosh_op = base_ops.create_unary_op( + name="arccosh", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +arctanh_op = base_ops.create_unary_op( + name="arctanh", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +floor_op = base_ops.create_unary_op( + name="floor", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +ceil_op = base_ops.create_unary_op( + name="ceil", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +abs_op = base_ops.create_unary_op(name="abs", type_signature=op_typing.UNARY_NUMERIC) + +pos_op = base_ops.create_unary_op(name="pos", type_signature=op_typing.UNARY_NUMERIC) + +neg_op = base_ops.create_unary_op(name="neg", type_signature=op_typing.UNARY_NUMERIC) + +exp_op = base_ops.create_unary_op( + name="exp", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +expm1_op = base_ops.create_unary_op( + name="expm1", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +ln_op = base_ops.create_unary_op( + name="log", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +log10_op = base_ops.create_unary_op( + name="log10", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +log1p_op = base_ops.create_unary_op( + name="log1p", type_signature=op_typing.UNARY_REAL_NUMERIC +) + +sqrt_op = base_ops.create_unary_op( + name="sqrt", type_signature=op_typing.UNARY_REAL_NUMERIC +) + + +@dataclasses.dataclass(frozen=True) +class AddOp(base_ops.BinaryOp): + name: typing.ClassVar[str] = "add" + + def output_type(self, *input_types): + left_type = input_types[0] + right_type = input_types[1] + if all(map(dtypes.is_string_like, input_types)) and len(set(input_types)) == 1: + # String addition + return input_types[0] + if (left_type is None or dtypes.is_numeric(left_type)) and ( + right_type is None or dtypes.is_numeric(right_type) + ): + # Numeric addition + return dtypes.coerce_to_common(left_type, right_type) + # TODO: Add temporal addition once delta types supported + raise TypeError(f"Cannot add dtypes {left_type} and {right_type}") + + +add_op = AddOp() + + +@dataclasses.dataclass(frozen=True) +class SubOp(base_ops.BinaryOp): + name: typing.ClassVar[str] = "sub" + + # Note: this is actualyl a vararg op, but we don't model that yet + def output_type(self, *input_types): + left_type = input_types[0] + right_type = input_types[1] + if (left_type is None or dtypes.is_numeric(left_type)) and ( + right_type is None or dtypes.is_numeric(right_type) + ): + # Numeric subtraction + return dtypes.coerce_to_common(left_type, right_type) + # TODO: Add temporal addition once delta types supported + raise TypeError(f"Cannot subtract dtypes {left_type} and {right_type}") + + +sub_op = SubOp() + +mul_op = base_ops.create_binary_op(name="mul", type_signature=op_typing.BINARY_NUMERIC) + +div_op = base_ops.create_binary_op( + name="div", type_signature=op_typing.BINARY_REAL_NUMERIC +) + +floordiv_op = base_ops.create_binary_op( + name="floordiv", type_signature=op_typing.BINARY_NUMERIC +) + +pow_op = base_ops.create_binary_op(name="pow", type_signature=op_typing.BINARY_NUMERIC) + +mod_op = base_ops.create_binary_op(name="mod", type_signature=op_typing.BINARY_NUMERIC) + +arctan2_op = base_ops.create_binary_op( + name="arctan2", type_signature=op_typing.BINARY_REAL_NUMERIC +) + +round_op = base_ops.create_binary_op( + name="round", type_signature=op_typing.BINARY_REAL_NUMERIC +) + +unsafe_pow_op = base_ops.create_binary_op( + name="unsafe_pow_op", type_signature=op_typing.BINARY_REAL_NUMERIC +) diff --git a/bigframes/operations/numpy_op_maps.py b/bigframes/operations/numpy_op_maps.py new file mode 100644 index 0000000000..7f3decdfa0 --- /dev/null +++ b/bigframes/operations/numpy_op_maps.py @@ -0,0 +1,55 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np + +from bigframes.operations import base_ops, generic_ops, numeric_ops + +# Just parameterless unary ops for now +# TODO: Parameter mappings +NUMPY_TO_OP: dict[np.ufunc, base_ops.UnaryOp] = { + np.sin: numeric_ops.sin_op, + np.cos: numeric_ops.cos_op, + np.tan: numeric_ops.tan_op, + np.arcsin: numeric_ops.arcsin_op, + np.arccos: numeric_ops.arccos_op, + np.arctan: numeric_ops.arctan_op, + np.sinh: numeric_ops.sinh_op, + np.cosh: numeric_ops.cosh_op, + np.tanh: numeric_ops.tanh_op, + np.arcsinh: numeric_ops.arcsinh_op, + np.arccosh: numeric_ops.arccosh_op, + np.arctanh: numeric_ops.arctanh_op, + np.exp: numeric_ops.exp_op, + np.log: numeric_ops.ln_op, + np.log10: numeric_ops.log10_op, + np.sqrt: numeric_ops.sqrt_op, + np.abs: numeric_ops.abs_op, + np.floor: numeric_ops.floor_op, + np.ceil: numeric_ops.ceil_op, + np.log1p: numeric_ops.log1p_op, + np.expm1: numeric_ops.expm1_op, +} + + +NUMPY_TO_BINOP: dict[np.ufunc, base_ops.BinaryOp] = { + np.add: numeric_ops.add_op, + np.subtract: numeric_ops.sub_op, + np.multiply: numeric_ops.mul_op, + np.divide: numeric_ops.div_op, + np.power: numeric_ops.pow_op, + np.arctan2: numeric_ops.arctan2_op, + np.maximum: generic_ops.maximum_op, + np.minimum: generic_ops.minimum_op, +} diff --git a/bigframes/operations/remote_function_ops.py b/bigframes/operations/remote_function_ops.py new file mode 100644 index 0000000000..0bced56f8d --- /dev/null +++ b/bigframes/operations/remote_function_ops.py @@ -0,0 +1,80 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import dataclasses +import typing + +from bigframes import dtypes +from bigframes.operations import base_ops + + +@dataclasses.dataclass(frozen=True) +class RemoteFunctionOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "remote_function" + func: typing.Callable + apply_on_null: bool + + def output_type(self, *input_types): + # This property should be set to a valid Dtype by the @remote_function decorator or read_gbq_function method + if hasattr(self.func, "output_dtype"): + if dtypes.is_array_like(self.func.output_dtype): + # TODO(b/284515241): remove this special handling to support + # array output types once BQ remote functions support ARRAY. + # Until then, use json serialized strings at the remote function + # level, and parse that to the intended output type at the + # bigframes level. + return dtypes.STRING_DTYPE + return self.func.output_dtype + else: + raise AttributeError("output_dtype not defined") + + +@dataclasses.dataclass(frozen=True) +class BinaryRemoteFunctionOp(base_ops.BinaryOp): + name: typing.ClassVar[str] = "binary_remote_function" + func: typing.Callable + + def output_type(self, *input_types): + # This property should be set to a valid Dtype by the @remote_function decorator or read_gbq_function method + if hasattr(self.func, "output_dtype"): + if dtypes.is_array_like(self.func.output_dtype): + # TODO(b/284515241): remove this special handling to support + # array output types once BQ remote functions support ARRAY. + # Until then, use json serialized strings at the remote function + # level, and parse that to the intended output type at the + # bigframes level. + return dtypes.STRING_DTYPE + return self.func.output_dtype + else: + raise AttributeError("output_dtype not defined") + + +@dataclasses.dataclass(frozen=True) +class NaryRemoteFunctionOp(base_ops.NaryOp): + name: typing.ClassVar[str] = "nary_remote_function" + func: typing.Callable + + def output_type(self, *input_types): + # This property should be set to a valid Dtype by the @remote_function decorator or read_gbq_function method + if hasattr(self.func, "output_dtype"): + if dtypes.is_array_like(self.func.output_dtype): + # TODO(b/284515241): remove this special handling to support + # array output types once BQ remote functions support ARRAY. + # Until then, use json serialized strings at the remote function + # level, and parse that to the intended output type at the + # bigframes level. + return dtypes.STRING_DTYPE + return self.func.output_dtype + else: + raise AttributeError("output_dtype not defined") diff --git a/bigframes/operations/semantics.py b/bigframes/operations/semantics.py index a2bf18a41d..3b7a77e5b7 100644 --- a/bigframes/operations/semantics.py +++ b/bigframes/operations/semantics.py @@ -21,12 +21,13 @@ import numpy as np from bigframes import dtypes, exceptions -from bigframes.core import guid +from bigframes.core import guid, log_adapter +@log_adapter.class_logger class Semantics: def __init__(self, df) -> None: - import bigframes + import bigframes # Import in the function body to avoid circular imports. import bigframes.dataframe if not bigframes.options.experiments.semantic_operators: @@ -1099,7 +1100,7 @@ def _validate_model(model): @staticmethod def _confirm_operation(row_count: int): """Raises OperationAbortedError when the confirmation fails""" - import bigframes + import bigframes # Import in the function body to avoid circular imports. threshold = bigframes.options.compute.semantic_ops_confirmation_threshold diff --git a/bigframes/operations/string_ops.py b/bigframes/operations/string_ops.py new file mode 100644 index 0000000000..b2ce0706ce --- /dev/null +++ b/bigframes/operations/string_ops.py @@ -0,0 +1,247 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import dataclasses +import typing + +import pandas as pd +import pyarrow as pa + +from bigframes import dtypes +from bigframes.operations import base_ops +import bigframes.operations.type as op_typing + +len_op = base_ops.create_unary_op( + name="len", + type_signature=op_typing.FixedOutputType( + dtypes.is_iterable, dtypes.INT_DTYPE, description="iterable" + ), +) + +reverse_op = base_ops.create_unary_op( + name="reverse", type_signature=op_typing.STRING_TRANSFORM +) + +lower_op = base_ops.create_unary_op( + name="lower", type_signature=op_typing.STRING_TRANSFORM +) + +upper_op = base_ops.create_unary_op( + name="upper", type_signature=op_typing.STRING_TRANSFORM +) + +strip_op = base_ops.create_unary_op( + name="strip", type_signature=op_typing.STRING_TRANSFORM +) + +isalnum_op = base_ops.create_unary_op( + name="isalnum", type_signature=op_typing.STRING_PREDICATE +) + +isalpha_op = base_ops.create_unary_op( + name="isalpha", type_signature=op_typing.STRING_PREDICATE +) + +isdecimal_op = base_ops.create_unary_op( + name="isdecimal", type_signature=op_typing.STRING_PREDICATE +) + +isdigit_op = base_ops.create_unary_op( + name="isdigit", type_signature=op_typing.STRING_PREDICATE +) + +isnumeric_op = base_ops.create_unary_op( + name="isnumeric", type_signature=op_typing.STRING_PREDICATE +) + +isspace_op = base_ops.create_unary_op( + name="isspace", type_signature=op_typing.STRING_PREDICATE +) + +islower_op = base_ops.create_unary_op( + name="islower", type_signature=op_typing.STRING_PREDICATE +) + +isupper_op = base_ops.create_unary_op( + name="isupper", type_signature=op_typing.STRING_PREDICATE +) + +rstrip_op = base_ops.create_unary_op( + name="rstrip", type_signature=op_typing.STRING_TRANSFORM +) + +lstrip_op = base_ops.create_unary_op( + name="lstrip", type_signature=op_typing.STRING_TRANSFORM +) + +capitalize_op = base_ops.create_unary_op( + name="capitalize", type_signature=op_typing.STRING_TRANSFORM +) + + +@dataclasses.dataclass(frozen=True) +class StrContainsOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "str_contains" + pat: str + + def output_type(self, *input_types): + return op_typing.STRING_PREDICATE.output_type(input_types[0]) + + +@dataclasses.dataclass(frozen=True) +class StrContainsRegexOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "str_contains_regex" + pat: str + + def output_type(self, *input_types): + return op_typing.STRING_PREDICATE.output_type(input_types[0]) + + +@dataclasses.dataclass(frozen=True) +class StrGetOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "str_get" + i: int + + def output_type(self, *input_types): + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) + + +@dataclasses.dataclass(frozen=True) +class StrPadOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "str_pad" + length: int + fillchar: str + side: typing.Literal["both", "left", "right"] + + def output_type(self, *input_types): + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) + + +@dataclasses.dataclass(frozen=True) +class ReplaceStrOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "str_replace" + pat: str + repl: str + + def output_type(self, *input_types): + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) + + +@dataclasses.dataclass(frozen=True) +class RegexReplaceStrOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "str_rereplace" + pat: str + repl: str + + def output_type(self, *input_types): + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) + + +@dataclasses.dataclass(frozen=True) +class StartsWithOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "str_startswith" + pat: typing.Sequence[str] + + def output_type(self, *input_types): + return op_typing.STRING_PREDICATE.output_type(input_types[0]) + + +@dataclasses.dataclass(frozen=True) +class StringSplitOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "str_split" + pat: typing.Sequence[str] + + def output_type(self, *input_types): + input_type = input_types[0] + if not isinstance(input_type, pd.StringDtype): + raise TypeError("field accessor input must be a string type") + arrow_type = dtypes.bigframes_dtype_to_arrow_dtype(input_type) + return pd.ArrowDtype(pa.list_(arrow_type)) + + +@dataclasses.dataclass(frozen=True) +class EndsWithOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "str_endswith" + pat: typing.Sequence[str] + + def output_type(self, *input_types): + return op_typing.STRING_PREDICATE.output_type(input_types[0]) + + +@dataclasses.dataclass(frozen=True) +class ZfillOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "str_zfill" + width: int + + def output_type(self, *input_types): + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) + + +@dataclasses.dataclass(frozen=True) +class StrFindOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "str_find" + substr: str + start: typing.Optional[int] + end: typing.Optional[int] + + def output_type(self, *input_types): + signature = op_typing.FixedOutputType( + dtypes.is_string_like, dtypes.INT_DTYPE, "string-like" + ) + return signature.output_type(input_types[0]) + + +@dataclasses.dataclass(frozen=True) +class StrExtractOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "str_extract" + pat: str + n: int = 1 + + def output_type(self, *input_types): + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) + + +@dataclasses.dataclass(frozen=True) +class StrSliceOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "str_slice" + start: typing.Optional[int] + end: typing.Optional[int] + + def output_type(self, *input_types): + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) + + +@dataclasses.dataclass(frozen=True) +class StrRepeatOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "str_repeat" + repeats: int + + def output_type(self, *input_types): + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) + + +@dataclasses.dataclass(frozen=True) +class StrConcatOp(base_ops.BinaryOp): + name: typing.ClassVar[str] = "str_concat" + + # Note: this is actualyl a vararg op, but we don't model that yet + def output_type(self, *input_types): + if not all(map(dtypes.is_string_like, input_types)): + raise TypeError("string concat requires string-like arguments") + if len(set(input_types)) != 1: + raise TypeError("string concat requires like-typed arguments") + return input_types[0] + + +strconcat_op = StrConcatOp() diff --git a/bigframes/operations/struct_ops.py b/bigframes/operations/struct_ops.py new file mode 100644 index 0000000000..0926142b17 --- /dev/null +++ b/bigframes/operations/struct_ops.py @@ -0,0 +1,65 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import dataclasses +import typing + +import pandas as pd +import pyarrow as pa + +from bigframes import dtypes +from bigframes.operations import base_ops + + +@dataclasses.dataclass(frozen=True) +class StructFieldOp(base_ops.UnaryOp): + name: typing.ClassVar[str] = "struct_field" + name_or_index: typing.Union[str, int] + + def output_type(self, *input_types): + input_type = input_types[0] + if not isinstance(input_type, pd.ArrowDtype): + raise TypeError("field accessor input must be a struct type") + + pa_type = input_type.pyarrow_dtype + if not isinstance(pa_type, pa.StructType): + raise TypeError("field accessor input must be a struct type") + + pa_result_type = pa_type[self.name_or_index].type + return dtypes.arrow_dtype_to_bigframes_dtype(pa_result_type) + + +@dataclasses.dataclass(frozen=True) +class StructOp(base_ops.NaryOp): + name: typing.ClassVar[str] = "struct" + column_names: tuple[str] + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + num_input_types = len(input_types) + # value1, value2, ... + assert num_input_types == len(self.column_names) + fields = [] + + for i in range(num_input_types): + arrow_type = dtypes.bigframes_dtype_to_arrow_dtype(input_types[i]) + fields.append( + pa.field( + self.column_names[i], + arrow_type, + nullable=(not pa.types.is_list(arrow_type)), + ) + ) + return pd.ArrowDtype( + pa.struct(fields) + ) # [(name1, value1), (name2, value2), ...] diff --git a/bigframes/operations/time_ops.py b/bigframes/operations/time_ops.py new file mode 100644 index 0000000000..a6a65ad80e --- /dev/null +++ b/bigframes/operations/time_ops.py @@ -0,0 +1,40 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from bigframes import dtypes +from bigframes.operations import base_ops +import bigframes.operations.type as op_typing + +hour_op = base_ops.create_unary_op( + name="hour", + type_signature=op_typing.TIMELIKE_ACCESSOR, +) + +minute_op = base_ops.create_unary_op( + name="minute", + type_signature=op_typing.TIMELIKE_ACCESSOR, +) + +second_op = base_ops.create_unary_op( + name="second", + type_signature=op_typing.TIMELIKE_ACCESSOR, +) + +normalize_op = base_ops.create_unary_op( + name="normalize", + type_signature=op_typing.TypePreserving( + dtypes.is_time_like, + description="time-like", + ), +) diff --git a/bigframes/pandas/io/api.py b/bigframes/pandas/io/api.py index 2cb33beb04..454b2e729e 100644 --- a/bigframes/pandas/io/api.py +++ b/bigframes/pandas/io/api.py @@ -325,7 +325,10 @@ def read_parquet( read_parquet.__doc__ = inspect.getdoc(bigframes.session.Session.read_parquet) -def read_gbq_function(function_name: str, is_row_processor: bool = False): +def read_gbq_function( + function_name: str, + is_row_processor: bool = False, +): return global_session.with_default_session( bigframes.session.Session.read_gbq_function, function_name=function_name, diff --git a/bigframes/series.py b/bigframes/series.py index 842962f78a..46847996f1 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1062,7 +1062,9 @@ def mode(self) -> Series: # Approach: Count each value, return each value for which count(x) == max(counts)) block, agg_ids = block.aggregate( by_column_ids=[self._value_column], - aggregations=((self._value_column, agg_ops.count_op),), + aggregations=( + ex.UnaryAggregation(agg_ops.count_op, ex.deref(self._value_column)), + ), ) value_count_col_id = agg_ids[0] block, max_value_count_col_id = block.apply_window_op( @@ -1299,6 +1301,9 @@ def __getattr__(self, key: str): if key == "_block": raise AttributeError(key) elif hasattr(pandas.Series, key): + log_adapter.submit_pandas_labels( + self._block.expr.session.bqclient, self.__class__.__name__, key + ) raise AttributeError( textwrap.dedent( f""" @@ -1344,6 +1349,8 @@ def value_counts( def sort_values( self, *, axis=0, ascending=True, kind: str = "quicksort", na_position="last" ) -> Series: + if axis != 0 and axis != "index": + raise ValueError(f"No axis named {axis} for object type Series") if na_position not in ["first", "last"]: raise ValueError("Param na_position must be one of 'first' or 'last'") block = self._block.order_by( @@ -1358,6 +1365,8 @@ def sort_values( @validations.requires_index def sort_index(self, *, axis=0, ascending=True, na_position="last") -> Series: # TODO(tbergeron): Support level parameter once multi-index introduced. + if axis != 0 and axis != "index": + raise ValueError(f"No axis named {axis} for object type Series") if na_position not in ["first", "last"]: raise ValueError("Param na_position must be one of 'first' or 'last'") block = self._block @@ -1513,6 +1522,18 @@ def apply( ops.RemoteFunctionOp(func=func, apply_on_null=True) ) + # if the output is an array, reconstruct it from the json serialized + # string form + if bigframes.dtypes.is_array_like(func.output_dtype): + import bigframes.bigquery as bbq + + result_dtype = bigframes.dtypes.arrow_dtype_to_bigframes_dtype( + func.output_dtype.pyarrow_dtype.value_type + ) + result_series = bbq.json_extract_string_array( + result_series, value_dtype=result_dtype + ) + return result_series def combine( @@ -1541,6 +1562,18 @@ def combine( other, ops.BinaryRemoteFunctionOp(func=func) ) + # if the output is an array, reconstruct it from the json serialized + # string form + if bigframes.dtypes.is_array_like(func.output_dtype): + import bigframes.bigquery as bbq + + result_dtype = bigframes.dtypes.arrow_dtype_to_bigframes_dtype( + func.output_dtype.pyarrow_dtype.value_type + ) + result_series = bbq.json_extract_string_array( + result_series, value_dtype=result_dtype + ) + return result_series @validations.requires_index @@ -1644,7 +1677,8 @@ def unique(self, keep_order=True) -> Series: return self.drop_duplicates() block, result = self._block.aggregate( [self._value_column], - [(self._value_column, agg_ops.AnyValueOp())], + [ex.UnaryAggregation(agg_ops.AnyValueOp(), ex.deref(self._value_column))], + column_labels=self._block.column_labels, dropna=False, ) return Series(block.select_columns(result).reset_index()) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index d787f8e7f3..1d85967729 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -269,6 +269,7 @@ def __init__( storage_manager=self._temp_storage_manager, default_index_type=self._default_index_type, scan_index_uniqueness=self._strictly_ordered, + force_total_order=self._strictly_ordered, metrics=self._metrics, ) @@ -1250,12 +1251,19 @@ def remote_function( `$ gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID" --role="roles/run.invoker"`. Args: - input_types (type or sequence(type)): + input_types (type or sequence(type), Optional): For scalar user defined function it should be the input type or - sequence of input types. For row processing user defined function, - type `Series` should be specified. - output_type (type): - Data type of the output in the user defined function. + sequence of input types. The supported scalar input types are + `bool`, `bytes`, `float`, `int`, `str`. For row processing user + defined function (i.e. functions that receive a single input + representing a row in form of a Series), type `Series` should be + specified. + output_type (type, Optional): + Data type of the output in the user defined function. If the + user defined function returns an array, then `list[type]` should + be specified. The supported output types are `bool`, `bytes`, + `float`, `int`, `str`, `list[bool]`, `list[float]`, `list[int]` + and `list[str]`. dataset (str, Optional): Dataset in which to create a BigQuery remote function. It should be in `.` or `` format. If this diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index 6a5ba3f4c7..8fcc36b4d3 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -29,7 +29,6 @@ import google.cloud.bigquery as bigquery import google.cloud.bigquery.table -import bigframes from bigframes.core import log_adapter import bigframes.core.compile.googlesql as googlesql import bigframes.core.sql diff --git a/bigframes/session/_io/bigquery/read_gbq_table.py b/bigframes/session/_io/bigquery/read_gbq_table.py index 6114427570..ac9523243e 100644 --- a/bigframes/session/_io/bigquery/read_gbq_table.py +++ b/bigframes/session/_io/bigquery/read_gbq_table.py @@ -27,7 +27,6 @@ import google.api_core.exceptions import google.cloud.bigquery as bigquery -import bigframes import bigframes.clients import bigframes.core.compile import bigframes.core.compile.default_ordering diff --git a/bigframes/session/loader.py b/bigframes/session/loader.py index ec922e286d..43faae37c3 100644 --- a/bigframes/session/loader.py +++ b/bigframes/session/loader.py @@ -43,12 +43,14 @@ import bigframes.core.compile import bigframes.core.expression as expression import bigframes.core.guid +import bigframes.core.ordering import bigframes.core.pruning import bigframes.core.schema as schemata import bigframes.dataframe import bigframes.dtypes import bigframes.exceptions import bigframes.formatting_helpers as formatting_helpers +import bigframes.operations import bigframes.operations.aggregations as agg_ops import bigframes.session._io.bigquery as bf_io_bigquery import bigframes.session._io.bigquery.read_gbq_table as bf_read_gbq_table @@ -116,12 +118,14 @@ def __init__( storage_manager: bigframes.session.temp_storage.TemporaryGbqStorageManager, default_index_type: bigframes.enums.DefaultIndexKind, scan_index_uniqueness: bool, + force_total_order: bool, metrics: Optional[bigframes.session.metrics.ExecutionMetrics] = None, ): self._bqclient = bqclient self._storage_manager = storage_manager self._default_index_type = default_index_type self._scan_index_uniqueness = scan_index_uniqueness + self._force_total_order = force_total_order self._df_snapshot: Dict[ bigquery.TableReference, Tuple[datetime.datetime, bigquery.Table] ] = {} @@ -439,6 +443,21 @@ def read_gbq_table( primary_key=index_cols if is_index_unique else (), session=self._session, ) + # if we don't have a unique index, we order by row hash if we are in strict mode + if self._force_total_order: + if not is_index_unique: + array_value = array_value.order_by( + [ + bigframes.core.ordering.OrderingExpression( + bigframes.operations.RowKey().as_expr( + *(id for id in array_value.column_ids) + ), + # More concise SQL this way. + na_last=False, + ) + ], + is_total_order=True, + ) # ---------------------------------------------------- # Create Default Sequential Index if still have no index diff --git a/bigframes/streaming/dataframe.py b/bigframes/streaming/dataframe.py index 960da9f57c..90c638b82e 100644 --- a/bigframes/streaming/dataframe.py +++ b/bigframes/streaming/dataframe.py @@ -23,10 +23,10 @@ from google.cloud import bigquery -import bigframes from bigframes import dataframe from bigframes.core import log_adapter import bigframes.exceptions as bfe +import bigframes.session def _return_type_wrapper(method, cls): @@ -55,7 +55,7 @@ def _curate_df_doc(doc: Optional[str]): class StreamingBase: sql: str - _session: bigframes.Session + _session: bigframes.session.Session def to_bigtable( self, @@ -209,7 +209,7 @@ class StreamingDataFrame(StreamingBase): def __init__(self, df: dataframe.DataFrame, *, create_key=0): if create_key is not StreamingDataFrame._create_key: raise ValueError( - "StreamingDataFrame class shouldn't be created through constructor. Call bigframes.Session.read_gbq_table_streaming method to create." + "StreamingDataFrame class shouldn't be created through constructor. Call bigframes.pandas.read_gbq_table_streaming method to create." ) self._df = df self._df._disable_cache_override = True @@ -279,7 +279,7 @@ def _to_bigtable( instance: str, table: str, service_account_email: Optional[str] = None, - session: Optional[bigframes.Session] = None, + session: Optional[bigframes.session.Session] = None, app_profile: Optional[str] = None, truncate: bool = False, overwrite: bool = False, @@ -311,7 +311,7 @@ def _to_bigtable( Example: accountname@projectname.gserviceaccounts.com If not provided, the user account will be used, but this limits the lifetime of the continuous query. - session (bigframes.Session, default None): + session (bigframes.session.Session, default None): The session object to use for the query. This determines the project id and location of the query. If None, will default to the bigframes global session. @@ -414,7 +414,7 @@ def _to_pubsub( *, topic: str, service_account_email: str, - session: Optional[bigframes.Session] = None, + session: Optional[bigframes.session.Session] = None, job_id: Optional[str] = None, job_id_prefix: Optional[str] = None, ) -> bigquery.QueryJob: @@ -440,7 +440,7 @@ def _to_pubsub( service_account_email (str): Full name of the service account to run the continuous query. Example: accountname@projectname.gserviceaccounts.com - session (bigframes.Session, default None): + session (bigframes.session.Session, default None): The session object to use for the query. This determines the project id and location of the query. If None, will default to the bigframes global session. diff --git a/bigframes/version.py b/bigframes/version.py index 0858c02c1e..50dde36b01 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.32.0" +__version__ = "1.33.0" diff --git a/notebooks/getting_started/bq_dataframes_template.ipynb b/notebooks/getting_started/bq_dataframes_template.ipynb index dab4a7572f..2b47c40397 100644 --- a/notebooks/getting_started/bq_dataframes_template.ipynb +++ b/notebooks/getting_started/bq_dataframes_template.ipynb @@ -158,7 +158,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "metadata": { "id": "PyQmSRbKA8r-" }, @@ -242,11 +242,24 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "metadata": { "id": "Vyex9BQI-BNa" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 9c49a31b-7db6-49e1-b711-42eeebfdf7d3 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# This is how you read a BigQuery table\n", "df = bpd.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")\n", @@ -264,13 +277,13 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job bbb5d053-39a1-4542-83e1-f8c86ba93e0e is DONE. 28.9 kB processed. Open Job" + "Query job a4004810-9249-4fe3-ab87-7cc33b69808d is DONE. 28.9 kB processed. Open Job" ], "text/plain": [ "" @@ -282,7 +295,7 @@ { "data": { "text/html": [ - "Query job 099f1e48-07b6-481d-95af-e6d6811a54f4 is DONE. 31.7 kB processed. Open Job" + "Query job 09214d4a-8911-41b3-9f14-1c781cb7dc1b is DONE. 31.7 kB processed. Open Job" ], "text/plain": [ "" @@ -323,76 +336,76 @@ " \n", " \n", " \n", - " 104\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", + " 171\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 52.7\n", - " 19.8\n", - " 197.0\n", - " 3725.0\n", + " 41.1\n", + " 19.0\n", + " 182.0\n", + " 3425.0\n", " MALE\n", " \n", " \n", - " 271\n", + " 219\n", " Gentoo penguin (Pygoscelis papua)\n", " Biscoe\n", - " 59.6\n", - " 17.0\n", - " 230.0\n", - " 6050.0\n", - " MALE\n", + " 45.7\n", + " 13.9\n", + " 214.0\n", + " 4400.0\n", + " FEMALE\n", " \n", " \n", - " 146\n", + " 59\n", " Gentoo penguin (Pygoscelis papua)\n", " Biscoe\n", - " 46.6\n", - " 14.2\n", - " 210.0\n", - " 4850.0\n", - " FEMALE\n", - " \n", - " \n", - " 278\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Torgersen\n", - " 44.1\n", - " 18.0\n", - " 210.0\n", - " 4000.0\n", + " 49.9\n", + " 16.1\n", + " 213.0\n", + " 5400.0\n", " MALE\n", " \n", " \n", - " 337\n", + " 132\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Biscoe\n", - " 38.2\n", - " 20.0\n", - " 190.0\n", + " 39.6\n", + " 20.7\n", + " 191.0\n", " 3900.0\n", - " MALE\n", + " FEMALE\n", + " \n", + " \n", + " 223\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 47.3\n", + " 13.8\n", + " 216.0\n", + " 4725.0\n", + " <NA>\n", " \n", " \n", "\n", "" ], "text/plain": [ - " species island culmen_length_mm \\\n", - "104 Chinstrap penguin (Pygoscelis antarctica) Dream 52.7 \n", - "271 Gentoo penguin (Pygoscelis papua) Biscoe 59.6 \n", - "146 Gentoo penguin (Pygoscelis papua) Biscoe 46.6 \n", - "278 Adelie Penguin (Pygoscelis adeliae) Torgersen 44.1 \n", - "337 Adelie Penguin (Pygoscelis adeliae) Biscoe 38.2 \n", + " species island culmen_length_mm \\\n", + "171 Adelie Penguin (Pygoscelis adeliae) Dream 41.1 \n", + "219 Gentoo penguin (Pygoscelis papua) Biscoe 45.7 \n", + "59 Gentoo penguin (Pygoscelis papua) Biscoe 49.9 \n", + "132 Adelie Penguin (Pygoscelis adeliae) Biscoe 39.6 \n", + "223 Gentoo penguin (Pygoscelis papua) Biscoe 47.3 \n", "\n", " culmen_depth_mm flipper_length_mm body_mass_g sex \n", - "104 19.8 197.0 3725.0 MALE \n", - "271 17.0 230.0 6050.0 MALE \n", - "146 14.2 210.0 4850.0 FEMALE \n", - "278 18.0 210.0 4000.0 MALE \n", - "337 20.0 190.0 3900.0 MALE " + "171 19.0 182.0 3425.0 MALE \n", + "219 13.9 214.0 4400.0 FEMALE \n", + "59 16.1 213.0 5400.0 MALE \n", + "132 20.7 191.0 3900.0 FEMALE \n", + "223 13.8 216.0 4725.0 " ] }, - "execution_count": 23, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -441,7 +454,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 4, "metadata": { "id": "YKwCW7Nsavap" }, @@ -449,7 +462,7 @@ { "data": { "text/html": [ - "Query job 747e82a9-d9f9-4448-ab8c-a8ea75e6417d is DONE. 2.7 kB processed. Open Job" + "Query job 788957fa-55af-40af-a17c-913c1d0ec170 is DONE. 2.7 kB processed. Open Job" ], "text/plain": [ "" @@ -462,7 +475,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "average_body_mass: 4201.75438596491\n" + "average_body_mass: 4201.754385964913\n" ] } ], @@ -482,7 +495,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 5, "metadata": { "id": "4PyKMR61-Mjy" }, @@ -490,7 +503,7 @@ { "data": { "text/html": [ - "Query job 505bd504-f1fb-4d23-bad4-5f9f69164fec is DONE. 15.6 kB processed. Open Job" + "Query job 0026583d-b326-4393-82f9-a1d2629fa745 is DONE. 15.6 kB processed. Open Job" ], "text/plain": [ "" @@ -502,7 +515,7 @@ { "data": { "text/html": [ - "Query job 4fb709da-21b5-462e-8df5-394e14470f89 is DONE. 163 Bytes processed. Open Job" + "Query job 30e17097-f818-4e87-a4a1-3eef82bc38be is DONE. 163 Bytes processed. Open Job" ], "text/plain": [ "" @@ -567,7 +580,7 @@ "[3 rows x 1 columns]" ] }, - "execution_count": 25, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -585,6 +598,143 @@ "You can confirm that the calculations were run in BigQuery by clicking \"Open job\" from the previous cells' output. This takes you to the BigQuery console to view the SQL statement and job details." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Using SQL functions\n", + "\n", + "The [bigframes.bigquery module](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery) provides many [BigQuery SQL functions](https://cloud.google.com/bigquery/docs/reference/standard-sql/functions-all) which may not have a pandas-equivalent." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "import bigframes.bigquery" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `bigframes.bigquery.struct()` function creates a new STRUCT Series with subfields for each column in a DataFrames." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 223af6b4-d58d-42c3-b7c9-13a303536e21 is DONE. 31.7 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 96fb4ef9-6c12-4cfa-aa2c-16377efed8f3 is DONE. 11.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "133 {'culmen_length_mm': None, 'culmen_depth_mm': ...\n", + "279 {'culmen_length_mm': 37.9, 'culmen_depth_mm': ...\n", + "34 {'culmen_length_mm': 37.8, 'culmen_depth_mm': ...\n", + "96 {'culmen_length_mm': 37.7, 'culmen_depth_mm': ...\n", + "18 {'culmen_length_mm': 38.8, 'culmen_depth_mm': ...\n", + "dtype: struct[pyarrow]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lengths = bigframes.bigquery.struct(\n", + " df[[\"culmen_length_mm\", \"culmen_depth_mm\", \"flipper_length_mm\"]]\n", + ")\n", + "lengths.peek()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Use the `bigframes.bigquery.sql_scalar()` function to access arbitrary SQL syntax representing a single column expression." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 81db1901-0704-4af2-8395-4c310e043f30 is DONE. 34.5 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job dfb40bba-3170-47b8-9ab4-c3ed5ab7550e is DONE. 8.2 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "116 \n", + "308 15.5\n", + "285 15.9\n", + "2 16.0\n", + "245 16.1\n", + "dtype: Float64" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "shortest = bigframes.bigquery.sql_scalar(\n", + " \"LEAST({0}, {1}, {2})\",\n", + " columns=[df['culmen_depth_mm'], df['culmen_length_mm'], df['flipper_length_mm']],\n", + ")\n", + "shortest.peek()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -603,13 +753,13 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 33027eda-7ca4-4b39-8618-b211d8fe3ee8 is DONE. 28.9 kB processed. Open Job" + "Query job 4420834b-8f6f-46ce-9488-a7ae3960e72b is DONE. 34.5 kB processed. Open Job" ], "text/plain": [ "" @@ -624,13 +774,13 @@ "" ] }, - "execution_count": 50, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjAAAAGzCAYAAAAxPS2EAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOxdd5gURfp+u3tmNu+SliVIjiuKCihiROXEO/SUUzHCqZhBRU9RPANnBrNi5E5QfpjvxASY8TwjYjgEBURgyXHZvDsz3f37Y7qqq6qre3pmZ5eF6/d5eNiZ6a6uTlVfvd/7fZ9imqaJAAECBAgQIECAvQjqnu5AgAABAgQIECBAqggMmAABAgQIECDAXofAgAkQIECAAAEC7HUIDJgAAQIECBAgwF6HwIAJECBAgAABAux1CAyYAAECBAgQIMBeh8CACRAgQIAAAQLsdQgMmAABAgQIECDAXofAgAkQIECAAAEC7HUIDJgAAQLsUSiKgqlTp2akrerqalx88cXo0KEDFEXBpEmTMtJugAABWh4CAyZAgBaM2bNnQ1EUZGdnY+PGjY7fhw8fjgMOOGAP9Kxl4p577sHs2bNxxRVXYM6cORg7dmyTHOfJJ5/E7Nmzm6TtAAEC+ENoT3cgQIAAydHQ0ID77rsPjz/++J7uSsZRV1eHUCgzQ9HHH3+Mww8/HLfffntG2nPDk08+iXbt2uGCCy5o0uMECBDAHQEDEyDAXoCDDz4YM2fOxKZNm/Z0VzICwzBQX18PAMjOzs6YAbNt2za0atUqI201N0zTRF1d3Z7uRoAAew0CAyZAgL0AN998M3Rdx3333ee53dq1a6EoitS9IWpNpk6dCkVRsHLlSpx//vkoKipCcXExbr31VpimifXr1+PUU09FYWEhOnTogAcffNDRZkNDA26//Xb07t0bWVlZ6NKlCyZPnoyGhgbHsSdOnIi5c+diwIAByMrKwsKFC6X9AoCNGzdi/Pjx6NSpE7KystCjRw9cccUViEaj0vNetGgRFEXBmjVr8O6770JRFCiKgrVr16bUz1mzZuH4449H+/btkZWVhf333x9PPfUUt0337t2xbNkyfPrpp/Q4w4cP566pCOIKJP0h7Zx88sl47733MGTIEOTk5OCZZ54BAOzevRuTJk1Cly5dkJWVhd69e2PatGkwDINr9+WXX8bgwYNRUFCAwsJCHHjggXj00Uel1yhAgH0NgQspQIC9AD169MC4ceMwc+ZM3HTTTejUqVPG2j7rrLNQWlqK++67D++++y7uuusutGnTBs888wyOP/54TJs2DXPnzsX111+PQw89FMcccwyABIvyxz/+Ef/5z39w6aWXorS0FEuXLsXDDz+MlStXYt68edxxPv74Y7z66quYOHEi2rVrh+7du0v7s2nTJhx22GHYvXs3Lr30UvTv3x8bN27E66+/jtraWkQiEcc+paWlmDNnDq699lrst99++Mtf/gIAKC4uTqmfTz31FAYMGIA//vGPCIVCePvtt3HllVfCMAxMmDABAPDII4/gqquuQn5+Pv76178CAEpKStK69itWrMA555yDyy67DJdccgn69euH2tpaHHvssdi4cSMuu+wydO3aFV988QWmTJmCzZs345FHHgEAfPDBBzjnnHNwwgknYNq0aQCAn3/+GZ9//jmuueaatPoTIMBeBTNAgAAtFrNmzTIBmIsXLzZXr15thkIh8+qrr6a/H3vsseaAAQPo5zVr1pgAzFmzZjnaAmDefvvt9PPtt99uAjAvvfRS+l08Hjf3228/U1EU87777qPfl5eXmzk5Oeaf//xn+t2cOXNMVVXNzz77jDvO008/bQIwP//8c+7Yqqqay5YtS9qvcePGmaqqmosXL3ZsaxiG4zsW3bp1M0eNGsV9l0o/a2trHW2OHDnS7NmzJ/fdgAEDzGOPPdaxLbmmIsh9XLNmDddXAObChQu5be+8804zLy/PXLlyJff9TTfdZGqaZpaVlZmmaZrXXHONWVhYaMbjccfxAgT4X0DgQgoQYC9Bz549MXbsWDz77LPYvHlzxtq9+OKL6d+apmHIkCEwTRPjx4+n37dq1Qr9+vXDb7/9Rr977bXXUFpaiv79+2PHjh303/HHHw8A+OSTT7jjHHvssdh///09+2IYBubNm4dTTjkFQ4YMcfwuc88kQyr9zMnJoX9XVFRgx44dOPbYY/Hbb7+hoqIi5WMnQ48ePTBy5EhHf48++mi0bt2a6++IESOg6zr+/e9/A0jck5qaGnzwwQcZ71eAAHsDAhdSgAB7EW655RbMmTMH9913X8a0Dl27duU+FxUVITs7G+3atXN8v3PnTvp51apV+Pnnn1FcXCxtd9u2bdznHj16JO3L9u3bUVlZmdHQ8FT6+fnnn+P222/H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qlI6V4KGhgZcf/316Ny5M/Ly8jB06FAsWrSI/k5cY++99x5KS0uRn5+Pk046CZs3b6bbxONxXH311WjVqhXatm2LG2+8EX/+859x2mmnAQAuuOACfPrpp3j00UehKIrjui1ZsgRDhgxBbm4ujjjiCKxYscJX39O9xoqi4JlnnsHJJ5+M3NxclJaW4ssvv8Svv/6K4cOHIy8vD0cccQRWr16d1jX1iyAPTAuAUZ14cYyamj3ckwABArQ01MV07H/be3vk2MvvGInciP9pYsqUKZg5cyYefvhhHHXUUdi8eTN++eWXtI+/evVqLFiwAAsXLsTq1atxxhln4LfffkPfvn3x6aef4osvvsBFF12EESNGYOjQoQCAM888Ezk5OViwYAGKiorwzDPP4IQTTsDKlSvRpk0b2u68efPwzjvvoLy8HGPGjMF9993nmKj9YOLEiVi+fDlefvlldOrUCW+88QZOOukkLF26FH369AEA1NbW4oEHHsCcOXOgqirOP/98XH/99Zg7dy4AYNq0aZg7dy5mzZqF0tJSPProo5g3bx6OO+44AMCjjz6KlStX4oADDsAdd9wBACguLqZGzF//+lc8+OCDKC4uxuWXX46LLroIn3/+eZNdYwC488478dBDD+Ghhx7CjTfeiHPPPRc9e/bElClT0LVrV1x00UWYOHEiFixYkPI19YvAgGkBMA3T+l/fwz0JECBAgPRQVVWFRx99FDNmzMCf//xnAECvXr1w1FFHcYxEKjAMA8899xwKCgqw//7747jjjsOKFSswf/58qKqKfv36Ydq0afjkk08wdOhQ/Oc//8E333yDbdu2ISsrCwDwwAMPYN68eXj99ddx6aWX0nZnz56NgoICAMDYsWPx0UcfpWzAlJWVYdasWSgrK0OnTp0AANdffz0WLlyIWbNm4Z577gGQSE749NNPo1evXgASRg8xRADg8ccfx5QpUzB69GgAwIwZMzB//nz6e1FRESKRCHJzc9GhQwdHP+6++24ce+yxAICbbroJo0aNQn19va8EcaleY4ILL7wQY8aMAQDceOONGDZsGG699VaMHDkSAHDNNdfgwgsv9H8x00BgwLQE6JbhYgR1TwIECMAjJ6xh+R0j99ix/eLnn39GQ0MDTjjhhIwdv3v37tTIAICSkhJomsYlQSspKcG2bdsAAD/++COqq6vRtm1brp26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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjAAAAGzCAYAAAAxPS2EAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOx9d7wVxfn+s2dPuf1eelGQXhWlWIgNkYj5qrFjxwKWCBpsEIyF2MUWorGhghJLjL9ITFSwYgEVJBZsIB1pV9qFW885u/v74+zMzszO7O45t3HNPp8PH+45uzs7u2d35p3nfd731SzLshAiRIgQIUKECNGCEGnuDoQIESJEiBAhQmSL0IAJESJEiBAhQrQ4hAZMiBAhQoQIEaLFITRgQoQIESJEiBAtDqEBEyJEiBAhQoRocQgNmBAhQoQIESJEi0NowIQIESJEiBAhWhxCAyZEiBAhQoQI0eIQGjAhQoQIESJEiBaH0IAJESJEs0LTNEybNq1B2qqsrMT48ePRsWNHaJqGSZMmNUi7IUKE2PsQGjAhQuzFmD17NjRNQ15eHjZu3OjaPmLECOy///7N0LO9E3fddRdmz56N3/3ud5gzZw4uuOCCRjnPo48+itmzZzdK2yFChAiGaHN3IESIEP6oq6vDPffcg4cffri5u9LgqKmpQTTaMEPRe++9h8MOOwy33nprg7SnwqOPPoq2bdvioosuatTzhAgRQo2QgQkRogXgoIMOwsyZM7Fp06bm7kqDwDRN1NbWAgDy8vIazIApLy9HWVlZg7TV1LAsCzU1Nc3djRAhWgxCAyZEiBaAG2+8EYZh4J577vHcb+3atdA0TereELUm06ZNg6ZpWLFiBc4//3yUlpaiXbt2uPnmm2FZFjZs2ICTTz4ZJSUl6NixIx544AFXm3V1dbj11lvRq1cvJBIJdOnSBZMnT0ZdXZ3r3BMnTsTzzz+PgQMHIpFIYN68edJ+AcDGjRsxbtw4dO7cGYlEAt27d8fvfvc7JJNJ6XUvWLAAmqZhzZo1eP3116FpGjRNw9q1a7Pq56xZszBy5Ei0b98eiUQCAwYMwGOPPcbt061bN3z77bf44IMP6HlGjBjB3VMRxBVI+kPaOfHEEzF//nwMGzYM+fn5eOKJJwAAu3btwqRJk9ClSxckEgn06tUL9957L0zT5Np96aWXMHToUBQXF6OkpAQHHHAAZsyYIb1HIUL80hC6kEKEaAHo3r07xo4di5kzZ+IPf/gDOnfu3GBtn3XWWejfvz/uuecevP7667jjjjvQunVrPPHEExg5ciTuvfdePP/887j++utx8MEH46ijjgKQYVF++9vf4uOPP8Zll12G/v37Y9myZXjooYewYsUKzJ07lzvPe++9h5dffhkTJ05E27Zt0a1bN2l/Nm3ahEMOOQS7du3CZZddhn79+mHjxo145ZVXUF1djXg87jqmf//+mDNnDq655hrsu+++uO666wAA7dq1y6qfjz32GAYOHIjf/va3iEaj+Pe//40rr7wSpmliwoQJAIA///nPuOqqq1BUVIQ//vGPAIAOHTrkdO+XL1+Oc845B5dffjkuvfRS9O3bF9XV1Tj66KOxceNGXH755ejatSsWLVqEqVOnYvPmzfjzn/8MAHj77bdxzjnn4Nhjj8W9994LAPj++++xcOFC/P73v8+pPyFCtChYIUKE2Gsxa9YsC4C1ZMkSa9WqVVY0GrWuvvpquv3oo4+2Bg4cSD+vWbPGAmDNmjXL1RYA69Zbb6Wfb731VguAddlll9Hv0um0te+++1qapln33HMP/X7nzp1Wfn6+deGFF9Lv5syZY0UiEeujjz7izvP4449bAKyFCxdy545EIta3337r26+xY8dakUjEWrJkiWtf0zRd37HYb7/9rBNOOIH7Lpt+VldXu9ocPXq01aNHD+67gQMHWkcffbRrX3JPRZDfcc2aNVxfAVjz5s3j9r399tutwsJCa8WKFdz3f/jDHyxd163169dblmVZv//9762SkhIrnU67zhcixP8CQhdSiBAtBD169MAFF1yAJ598Eps3b26wdsePH0//1nUdw4YNg2VZGDduHP2+rKwMffv2xerVq+l3//jHP9C/f3/069cP27Zto/9GjhwJAHj//fe58xx99NEYMGCAZ19M08TcuXNx0kknYdiwYa7tMveMH7LpZ35+Pv27oqIC27Ztw9FHH43Vq1ejoqIi63P7oXv37hg9erSrv0ceeSRatWrF9XfUqFEwDAMffvghgMxvUlVVhbfffrvB+xUiREtA6EIKEaIF4aabbsKcOXNwzz33NJjWoWvXrtzn0tJS5OXloW3btq7vt2/fTj//+OOP+P7779GuXTtpu+Xl5dzn7t27+/bl559/xu7duxs0NDybfi5cuBC3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n332QWFhIQ499FAsWLCAbieusfnz56N///4oKirC8ccfj82bN9N90uk0rr76apSVlaFNmzaYMmUKLrzwQpxyyikAgIsuuggffPABZsyYAU3TXPdt6dKlGDZsGAoKCvCrX/0Ky5cvD9T3XO+xpml44okncOKJJ6KgoAD9+/fHJ598gpUrV2LEiBEoLCzEr371K6xatSqne6rC9lmzUH7/A6iYO7dB2w0RIkSIhsb/ZB4YETUpAwNumd8s5/7uttEoiAf/GaZOnYqZM2fioYcewhFHHIHNmzfjhx9+yPn8q1atwptvvol58+Zh1apVOOOMM7B69Wr06dMHH3zwARYtWoRLLrkEo0aNwqGHHgoAOPPMM5Gfn48333wTpaWleOKJJ3DsscdixYoVaN26NW137ty5+M9//oOdO3dizJgxuOeee1wTdRBMnDgR3333HV566SV07twZr776Ko4//ngsW7YMvXv3BgBUV1fj/vvvx5w5cxCJRHD++efj+uuvx/PPPw8AuPfee/H8889j1qxZ6N+/P2bMmIG5c+fimGOOAQDMmDEDK1aswP7774/bbrsNANCuXTtqxPzxj3/EAw88gHbt2uGKK67AJZdcgoULFzbaPQaA22+/HQ8++CAefPBBTJkyBeeeey569OiBqVOnomvXrrjkkkswceJEvPnmm1nfUxXMyioAgFFZ2WBthggRIkRjIDRgWhD27NmDGTNm4JFHHsGFF14IAOjZsyeOOOIIjpHIBqZp4plnnkFxcTEGDBiAY445BsuXL8cbb7yBSCSCvn374t5778X777+PQw89FB9//DEWL16M8vJyJBIJAMD999+PuXPn4pVXXsFll11G2509ezaKi4sBABdccAHefffdrA2Y9evXY9asWVi/fj06d+4MALj++usxb948zJo1C3fddRcAIJVK4fHHH0fPnj0BZIweYogAwMMPP4ypU6fi1FNPBQA88sgjeOONN+j20tJSxONxFBQUoGPHjq5+3HnnnTj66KMBAH/4wx9wwgknoLa2NlCCuGzvMcHFF1+MMWPGAACmTJmC4cOH4+abb8bo0aMBAL///e9x8cUXB7+ZQUDqgpnZMYMhQoQI0dQIDRgA+TEd3902utnOHRTff/896urqcOyxxzbY+bt160aNDADo0KEDdF3nkqB16NAB5eXlAICvvvoKlZWVaNOmDddO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", "text/plain": [ "
" ] @@ -645,13 +795,13 @@ }, { "cell_type": "code", - "execution_count": 75, + "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 2b3aba71-3b36-425a-835f-6c72fc79950a is DONE. 12.9 kB processed. Open Job" + "Query job 2e9d7f11-c442-4a6b-905f-0f5ac498b399 is DONE. 12.9 kB processed. Open Job" ], "text/plain": [ "" @@ -663,7 +813,7 @@ { "data": { "text/html": [ - "Query job 013405ab-25b3-4ead-8fe7-41974af67752 is DONE. 23.8 kB processed. Open Job" + "Query job 27db4f04-849b-4193-acae-6ecff5f4350f is DONE. 23.8 kB processed. Open Job" ], "text/plain": [ "" @@ -678,13 +828,13 @@ "" ] }, - "execution_count": 75, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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bN68udwdkp48eTLS0tIUX0lJSe/dtmiMw7t3RCaqaIo+3xzTQ0QV0ScdgYmOjsaTJ0+U7h+Tn5+P06dPY+nSpThy5AhycnKQmpqqdBTm8ePHsLCwAABYWFggMjJSab9FVym9u83fr1x6/Pgx5HL5eydV09bWhra29ke9D3V1dRgZGSnux6Onp8fJy6jCKCwsRGZmJp48eQIjIyOoq6sLHYmIqMR9UoFp27atyhTtQ4YMQc2aNREYGAhra2toamoiLCxMcYfhuLg4JCYmKm4a6Onpiblz5+LJkyeK8RShoaGQy+WKKe89PT1x8OBBpdcJDQ1VufHgf1FUlt69qSBRRWJkZKT4nBMRVTSfVGAMDAxQu3ZtpWWVKlWCiYmJYrmfnx/Gjx+PypUrQy6XY9SoUfD09ESTJk0AAO3bt4erqysGDhyIoKAgpKSkYMqUKRg5cqTiCMrw4cOxdOlSTJo0Cb6+vjh+/Di2b9+OAwcOlMR7BvD2hoKWlpYwMzPjjf+owtHU1OSRFyKq0Er8VgKLFi2CmpoaevXqhezsbHh7e2P58uWK9erq6ti/fz9GjBgBT09PVKpUCYMGDcKsWbMU29jb2+PAgQMYN24cFi9ejGrVqmHNmjXw9vYu6bhQV1fnD3oiIiKRkeStBIiIiMSA88CU0q0EiIiIiITAAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiU+I3cyQiEquSvu9Meb/nDJGY8QgMERERiQ6PwJAgpHiHVSIiKjk8AkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDRERE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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjAAAALECAYAAAAW8gpgAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAACEdklEQVR4nOzdeVxN+eM/8NdtL3UraaVVSRHKmm0skXVsw1hGqBhG1kFjxr5PnzFirGMLw9gGYyfZhiJF2UOiUPZKSvvvD7/u150bw0x1Op3X8/Ho8XDPOffc123u1Ktz3ud9ZIWFhYUgIiIiEhE1oQMQERERfSoWGCIiIhIdFhgiIiISHRYYIiIiEh0WGCIiIhIdFhgiIiISHRYYIiIiEh0NoQOUloKCAjx69AgGBgaQyWRCxyEiIqKPUFhYiFevXsHKygpqau8/zlJhC8yjR49gbW0tdAwiIiL6F5KSklCtWrX3rq+wBcbAwADA22+AXC4XOA0RERF9jPT0dFhbWyt+j79PhS0wRaeN5HI5CwwREZHI/NPwDw7iJSIiItFhgSEiIiLRYYEhIiIi0amwY2A+Vn5+PnJzc4WOQVSiNDU1oa6uLnQMIqJSI9kCU1hYiJSUFKSmpgodhahUGBkZwcLCgvMgEVGFJNkCU1RezMzMoKenxx/yVGEUFhYiMzMTT548AQBYWloKnIiIqORJssDk5+cryouJiYnQcYhKnK6uLgDgyZMnMDMz4+kkIqpwJDmIt2jMi56ensBJiEpP0eebY7yIqCKSZIEpwtNGVJHx801EFZmkCwwRERGJEwsMERERiY4kB/G+j913B8r09e4t6FymrxcSEoKxY8eW+0vHW7VqhXr16iE4OFjoKDh58iRat26Nly9fwsjISOg4RET0//EIDNH/16pVK4wdO1boGERE9BFYYIiIiEh0WGBEpqCgAEFBQXB0dIS2tjZsbGwwd+5cnDx5EjKZTOn0UExMDGQyGe7du1fsvmbMmIF69eph3bp1sLGxgb6+Pr755hvk5+cjKCgIFhYWMDMzw9y5c5Wel5qaCn9/f5iamkIul6NNmzaIjY1V2e+mTZtgZ2cHQ0ND9O3bF69evfpX7zk7OxsTJkxA1apVUalSJTRu3BgnT55UrA8JCYGRkRGOHDkCFxcX6Ovro0OHDkhOTlZsk5eXh9GjR8PIyAgmJiYIDAzEoEGD0L17dwDA4MGDcerUKSxevBgymUzl+xYdHY0GDRpAT08PTZs2RVxc3Edl/7ffY5lMhlWrVqFLly7Q09ODi4sLIiIicOfOHbRq1QqVKlVC06ZNER8f/6++p0REYscxMCIzefJkrF69GosWLULz5s2RnJyMmzdv/uv9xcfH49ChQzh8+DDi4+PxxRdf4O7du6hRowZOnTqF8PBw+Pr6wsvLC40bNwYA9O7dG7q6ujh06BAMDQ2xatUqtG3bFrdu3ULlypUV+92zZw/279+Ply9fok+fPliwYIHKL+qPERAQgOvXr2Pr1q2wsrLC7t270aFDB1y5cgVOTk4AgMzMTPz000/YtGkT1NTU8NVXX2HChAnYvHkzAODHH3/E5s2bsX79eri4uGDx4sXYs2cPWrduDQBYvHgxbt26hdq1a2PWrFkAAFNTU0WJ+eGHH7Bw4UKYmppi+PDh8PX1xdmzZ0vtewwAs2fPxs8//4yff/4ZgYGB6N+/PxwcHDB58mTY2NjA19cXAQEBOHTo0Cd/T4lIHG7UdCnxfbrcvFHi+xTCJx2BmTFjhuKv06KvmjVrKta/efMGI0eOhImJCfT19dGrVy88fvxYaR+JiYno3Lkz9PT0YGZmhokTJyIvL09pm5MnT8LDwwPa2tpwdHRESEjIv3+HFcirV6+wePFiBAUFYdCgQahevTqaN28Of3//f73PgoICrFu3Dq6urujatStat26NuLg4BAcHw9nZGUOGDIGzszNOnDgBADhz5gwiIyOxY8cONGjQAE5OTvjpp59gZGSEnTt3Ku03JCQEtWvXRosWLTBw4ECEhYV9cr7ExESsX78eO3bsQIsWLVC9enVMmDABzZs3x/r16xXb5ebmYuXKlWjQoAE8PDwQEBCg9Hq//PILJk+ejB49eqBmzZpYunSp0qBcQ0NDaGlpQU9PDxYWFrCwsFCavXbu3Ln47LPP4Orqiu+++w7h4eF48+ZNqXyPiwwZMgR9+vRBjRo1EBgYiHv37mHAgAHw9vaGi4sLxowZo3QkiohISj75CEytWrVw7Nix/9uBxv/tYty4cThw4AB27NgBQ0NDBAQEoGfPnoq/VPPz89G5c2dYWFggPDwcycnJ8PHxgaamJubNmwcASEhIQOfOnTF8+HBs3rwZYWFh8Pf3h6WlJby9vf/r+xW1GzduIDs7G23bti2xfdrZ2cHAwEDx2NzcHOrq6lBTU1NaVnRfndjYWGRkZKjcgiErK0vpdMbf92tpaanYx6e4cuUK8vPzUaNGDaXl2dnZShn09PRQvXr1Yl8vLS0Njx8/RqNGjRTr1dXVUb9+fRQUFHxUjjp16ijtG3g7Tb+Njc0/PvdTv8fFvaa5uTkAwM3NTWnZmzdvkJ6eDrlc/lHvg4ioovjkAqOhoQELCwuV5WlpaVi7di22bNmCNm3aAIDicP25c+fQpEkTHD16FNevX8exY8dgbm6OevXqYfbs2QgMDMSMGTOgpaWFlStXwt7eHgsXLgQAuLi44MyZM1i0aJHkC0zR/W2KU/TLsLCwULHsY6aQ19TUVHosk8mKXVb0iz4jIwOWlpbF/uX/7hGND+3jU2RkZEBdXR3R0dEq9/PR19f/4Ou9+734r97df9EMtx/7fj71e/yh1/wvOYiIKpJPHsR7+/ZtWFlZwcHBAQMGDEBiYiKAt4Mcc3Nz4eXlpdi2Zs2asLGxQUREBAAgIiICbm5uir8mAcDb2xvp6em4du2aYpt391G0TdE+3ic7Oxvp6elKXxWNk5MTdHV1iz0VY2pqCgBKA1djYmJKPIOHhwdSUlKgoaEBR0dHpa8qVaqU+Ou5u7sjPz8fT548UXm94op0cQwNDWFubo4LFy4oluXn5+PixYtK22lpaSE/P79E8xMRUen4pALTuHFjhISE4PDhw1ixYgUSEhLQokULvHr1CikpKdDS0lKZ7Mvc3BwpKSkAgJSUFKXyUrS+aN2HtklPT0dWVtZ7s82fPx+GhoaKL2tr6095a6Kgo6ODwMBATJo0CRs3bkR8fDzOnTuHtWvXwtHREdbW1pgxYwZu376NAwcOKI5ilSQvLy94enqie/fuOHr0KO7du4fw8HD88MMPiIqKKvHXq1GjBgYMGAAfHx/s2rULCQkJiIyMxPz583HgwMdPPDhq1CjMnz8ff/75J+Li4jBmzBi8fPlS6X5BdnZ2OH/+PO7du4dnz57xyAYRUTn2SaeQOnbsqPh3nTp10LhxY9ja2mL79u0fPL1RFiZPnozx48crHqenp39yiSnrmXH/jalTp0JDQwPTpk3Do0ePYGlpieHDh0NTUxO///47RowYgTp16qBhw4aYM2cOevfuXaKvL5PJcPDgQfzwww8YMmQInj59CgsLC7Rs2VKleJaU9evXY86cOfj222/x8OFDVKlSBU2aNEGXLl0+eh+BgYFISUmBj48P1NXVMWzYMHh7eyudlpowYQIGDRoEV1dXZGVlISEhoTTeDhERlQBZ4X8cKNCwYUN4eXmhXbt2aNu2rcqU67a2thg7dizGjRuHadOmYe/evUqnNhISEuDg4ICLFy/C3d0dLVu2hIeHh9I08uvXr8fYsWORlpb20bnS09NhaGiItLQ0lQGOb968QUJCAuzt7aGjo/Nv3zqJWEFBAVxcXNCnTx/Mnj1b6Dilgp9zIvGT4mXUH/r9/a7/NJFdRkYG4uPjYWlpifr160NTU1NpfEZcXBwSExPh6ekJAPD09MSVK1eUrrYIDQ2FXC6Hq6urYpu/j/EIDQ1V7IPo37h//z5Wr16NW7du4cqVKxgxYgQSEhLQv39/oaMREdG/8EkFZsKECTh16pRi3EOPHj2grq6Ofv36wdDQEH5+fhg/fjxOnDiB6OhoDBkyBJ6enmjSpAkAoH379nB1dcXAgQMRGxuLI0eOYMqUKRg5ciS0tbUBAMOHD8fdu3cxadIk3Lx5E8uXL8f27dsxbty4kn/3VOYSExOhr6//3q+iQeElTU1NDSEhIWjYsCGaNWuGK1eu4NixY3Bx+W9/3dSqVeu976VoEj0iIip5nzQG5sGDB+jXrx+eP38OU1NTNG/eHOfOnVNcAbNo0SKoqamhV69eyM7Ohre3N5YvX654vrq6Ovbv348RI0bA09MTlSpVwqBBgxQznwKAvb09Dhw4gHHjxmHx4sWoVq0a1qxZI/lLqCsKKyurD14dZWVlVSqva21t/dEz536KgwcPvvdy9dIaE0RERCUwBqa84hgYkjp+zonEj2NgSmkMDBEREZEQWGCIiIhIdFhgiIiISHRYYIiIiEh0WGCIiIhIdD75btQV2gzDMn69j59ZuCSEhIRg7NixSE1NLdPXLQmtWrVCvXr1lGZoLi0ymQy7d+9G9+7dS/21iIjo3+ERGJKsGTNmoF69ekLHICKif4EFhoiIiESHBUZkCgoKEBQUBEdHR2hra8PGxgZz587FyZMnIZPJlE4PxcTEQCaT4d69e8Xuq+gIxLp162BjYwN9fX188803yM/PR1BQECwsLGBmZoa5c+cqPS81NRX+/v4wNTWFXC5HmzZtEBsbq7LfTZs2wc7ODoaGhujbty9evXr1Ue/x9evX8PHxgb6+PiwtLbFw4UKVbbKzszFhwgRUrVoVlSpVQuPGjXHy5EnF+pCQEBgZGWHPnj1wcnKCjo4OvL29kZSUpFg/c+ZMxMbGQiaTQSaTISQkRPH8Z8+eoUePHtDT04OTkxP27t37UdmL/jscOXIE7u7u0NXVRZs2bfDkyRMcOnQILi4ukMvl6N+/PzIzMxXPa9WqFUaNGoWxY8fC2NgY5ubmWL16NV6/fo0hQ4bAwMAAjo6OOHTo0EflICKq6FhgRGby5MlYsGABpk6diuvXr2PLli3/acr6+Ph4HDp0CIcPH8bvv/+OtWvXonPnznjw4AFOnTqFH3/8EVOmTMH58+cVz+ndu7fiF3J0dDQ8PDzQtm1bvHjxQmm/e/bswf79+7F//36cOnUKCxYs+KhMEydOxKlTp/Dnn3/i6NGjOHnyJC5evKi0TUBAACIiIrB161ZcvnwZvXv3RocOHXD79m3FNpmZmZg7dy42btyIs2fPIjU1FX379gUAfPnll/j2229Rq1YtJCcnIzk5GV9++aXiuTNnzkSfPn1w+fJldOrUCQMGDFB6f/9kxowZWLp0KcLDw5GUlIQ+ffogODgYW7ZswYEDB3D06FH88ssvSs/ZsGEDqlSpgsjISIwaNQojRoxA79690bRpU1y8eBHt27fHwIEDlYoPEZFUscCIyKtXr7B48WIEBQVh0KBBqF69Opo3bw5/f/9/vc+CggKsW7cOrq6u6Nq1K1q3bo24uDgEBwfD2dkZQ4YMgbOzM06cOAEAOHPmDCIjI7Fjxw40aNAATk5O+Omnn2BkZISdO3cq7TckJAS1a9dGixYtMHDgQJW7jBcnIyMDa9euxU8//YS2bdvCzc0NGzZsQF5enmKbxMRErF+/Hjt27ECLFi1QvXp1TJgwAc2bN8f69esV2+Xm5mLp0qXw9PRE/fr1sWHDBoSHhyMyMhK6urrQ19eHhoYGLCwsYGFhAV1dXcVzBw8ejH79+sHR0RHz5s1DRkYGIiMjP/r7OmfOHDRr1gzu7u7w8/PDqVOnsGLFCri7u6NFixb44osvFN/TInXr1sWUKVPg5OSEyZMnQ0dHB1WqVMHQoUPh5OSEadOm4fnz57h8+fJH5yAiqqh4FZKI3LhxA9nZ2Wjbtm2J7dPOzg4GBgaKx+bm5lBXV4eamprSsidPngAAYmNjkZGRARMTE6X9ZGVlIT4+/r37tbS0VOzjQ+Lj45GTk4PGjRsrllWuXBnOzs6Kx1euXEF+fj5q1Kih9Nzs7GylXBoaGmjYsKHicc2aNWFkZIQbN26gUaNGH8xRp04dxb8rVaoEuVz+UfmLe765uTn09PTg4OCgtOzvhejd56irq8PExARubm5KzwHwSTmIiCoqFhgRefcIwd8VFY537835vrskv0tTU1PpsUwmK3ZZQUEBgLdHSCwtLZXGmxQxMjL64H6L9vFfZWRkQF1dHdHR0VBXV1dap6+vXyKv8V/zv/v8f/qefug1/74fACX2fSQiEjOeQhIRJycn6OrqFnsqxtTUFACQnJysWBYTE1PiGTw8PJCSkgINDQ04OjoqfVWpUuU/77969erQ1NRUGnPz8uVL3Lp1S/HY3d0d+fn5ePLkiUoGCwsLxXZ5eXmIiopSPI6Li0NqaipcXN7e3VVLSwv5+fn/OTMREZU9FhgR0dHRQWBgICZNmoSNGzciPj4e586dw9q1a+Ho6Ahra2vMmDEDt2/fxoEDB4q9eue/8vLygqenJ7p3746jR4/i3r17CA8Pxw8//KBUFv4tfX19+Pn5YeLEiTh+/DiuXr2KwYMHK53SqlGjBgYMGAAfHx/s2rULCQkJiIyMxPz583HgwAHFdpqamhg1ahTOnz+P6OhoDB48GE2aNFGcPrKzs0NCQgJiYmLw7NkzZGdn/+f8RERUNngK6V1lPDPuvzF16lRoaGhg2rRpePToESwtLTF8+HBoamri999/x4gRI1CnTh00bNgQc+bMQe/evUv09WUyGQ4ePIgffvgBQ4YMwdOnT2FhYYGWLVv+p6uh3vW///0PGRkZ6Nq1KwwMDPDtt98iLU35v8369esxZ84cfPvtt3j48CGqVKmCJk2aoEuXLopt9PT0EBgYiP79++Phw4do0aIF1q5dq1jfq1cv7Nq1C61bt0ZqairWr1+PwYMHl8h7ICKi0iUrfHfQRAWSnp4OQ0NDpKWlQS6XK6178+YNEhISYG9vDx0dHYESUmkS820TSgo/50Tid6OmS4nv0+XmjRLfZ0n60O/vd/EUEhEREYkOCwyVqcTEROjr67/3KzExUeiIHzR8+PD3Zh8+fLjQ8YiIJIOnkHhovUzl5eW999YGwNuBtRoa5Xdo1pMnT5Cenl7sOrlcDjMzszJO9H78nBOJH08hvf8UUvn9TUEVUtHl12JlZmZWrkoKEZFU8RQSERERiQ4LDBEREYkOCwwRERGJDgsMERERiQ4LDBEREYkOr0J6h9sGtzJ9vSuDrnzycwoLC/H1119j586dePnyJQwNDTF48GAEBwcDeHsZ8tixYzF27NiSDVsKZDIZdu/eje7duwsdBTNmzMCePXtK5QaYRERU8ngERmQOHz6MkJAQ7N+/H8nJyahdu7bS+gsXLmDYsGECpRMHmUyGPXv2CB2DiIj+Ax6BEZn4+HhYWlqiadOmAKAy6ZupqakQsVTk5ORAS0tL6BhERFRB8QiMiAwePBijRo1CYmIiZDIZ7OzsVLaxs7NTnE4C3h5tWLFiBTp27AhdXV04ODhg586divX37t2DTCbD1q1b0bRpU+jo6KB27do4deqU0n6vXr2Kjh07Ql9fH+bm5hg4cCCePXumWN+qVSsEBARg7NixqFKlCry9vT/5/SUlJaFPnz4wMjJC5cqV0a1bN6VZewcPHozu3bvjp59+gqWlJUxMTDBy5Ejk5uYqtklOTkbnzp2hq6sLe3t7bNmyRel7UvQ969GjR7Hfw02bNsHOzg6Ghobo27cvXr169VHZW7VqhVGjRmHs2LEwNjaGubk5Vq9ejdevX2PIkCEwMDCAo6MjDh06pHjOyZMnIZPJcOTIEbi7u0NXVxdt2rTBkydPcOjQIbi4uEAul6N///7IzMz85O8nEVFFxgIjIosXL8asWbNQrVo1JCcn48KFCx/1vKlTp6JXr16IjY3FgAED0LdvX9y4oTyV9MSJE/Htt9/i0qVL8PT0RNeuXfH8+XMAQGpqKtq0aQN3d3dERUXh8OHDePz4Mfr06aO0jw0bNkBLSwtnz57FypUrP+m95ebmwtvbGwYGBvjrr79w9uxZ6Ovro0OHDsjJyVFsd+LECcTHx+PEiRPYsGEDQkJCEBISoljv4+ODR48e4eTJk/jjjz/w66+/4smTJ4r1Rd+z9evXq3wP4+PjsWfPHuzfvx/79+/HqVOnsGDBgo9+Dxs2bECVKlUQGRmJUaNGYcSIEejduzeaNm2Kixcvon379hg4cKBKGZkxYwaWLl2K8PBwRYkLDg7Gli1bcODAARw9ehS//PLLJ30/iYgqOhYYETE0NISBgQHU1dVhYWHx0aeLevfuDX9/f9SoUQOzZ89GgwYNVH4hBgQEoFevXnBxccGKFStgaGiItWvXAgCWLl0Kd3d3zJs3DzVr1oS7uzvWrVuHEydO4NatW4p9ODk5ISgoCM7OznB2dv6k97Zt2zYUFBRgzZo1cHNzg4uLC9avX4/ExEScPHlSsZ2xsTGWLl2KmjVrokuXLujcuTPCwsIAADdv3sSxY8ewevVqNG7cGB4eHlizZg2ysrIUzy/6nhkZGal8DwsKChASEoLatWujRYsWGDhwoGLfH6Nu3bqYMmUKnJycMHnyZOjo6KBKlSoYOnQonJycMG3aNDx//hyXL19Wet6cOXPQrFkzuLu7w8/PD6dOncKKFSvg7u6OFi1a4IsvvsCJEyc+6ftJRFTRcQyMBHh6eqo8/vvVNu9uo6GhgQYNGiiO0sTGxuLEiRPQ19dX2Xd8fDxq1KgBAKhfv/6/zhgbG4s7d+7AwMBAafmbN28QHx+veFyrVi2oq6srHltaWuLKlbdXc8XFxUFDQwMeHh6K9Y6OjjA2Nv6oDHZ2dkqvb2lpqXT05p/UqVNH8W91dXWYmJjAze3/rmwzNzcHAJV9vvs8c3Nz6OnpwcHBQWlZZGTkR+cgIpICFhj6RxkZGejatSt+/PFHlXWWlpaKf1eqVOk/vUb9+vWxefNmlXXvHiXR1NRUWieTyVBQUPCvX/dd/3XfxT3/3WUymQwAVPb5921K8z0SEVUUPIUkAefOnVN57OLi8t5t8vLyEB0drdjGw8MD165dg52dHRwdHZW+/ktpeZeHhwdu374NMzMzldcwNDT8qH04OzsjLy8Ply5dUiy7c+cOXr58qbSdpqYm8vPzSyQ3EREJgwVGAnbs2IF169bh1q1bmD59OiIjIxEQEKC0zbJly7B7927cvHkTI0eOxMuXL+Hr6wsAGDlyJF68eIF+/frhwoULiI+Px5EjRzBkyJASKwIDBgxAlSpV0K1bN/z1119ISEjAyZMnMXr0aDx48OCj9lGzZk14eXlh2LBhiIyMxKVLlzBs2DDo6uoqjn4Ab08VhYWFISUlRaXcEBGROPAU0jv+zcy4YjBz5kxs3boV33zzDSwtLfH777/D1dVVaZsFCxZgwYIFiImJgaOjI/bu3YsqVaoAAKysrHD27FkEBgaiffv2yM7Ohq2tLTp06AA1tZLpwHp6ejh9+jQCAwPRs2dPvHr1ClWrVkXbtm0hl8s/ej8bN26En58fWrZsCQsLC8yfPx/Xrl2Djo6OYpuFCxdi/PjxWL16NapWrap0qTYREYmDrLCwsFDoEKUhPT0dhoaGSEtLU/kF+ObNGyQkJMDe3l7pF1tF9E/T9d+7dw/29va4dOkS6tWrV6bZysKDBw9gbW2NY8eOoW3btkLHKVNS+pwTVVQ3arr880afyOXmjX/eSEAf+v39Lh6BoQrl+PHjyMjIgJubG5KTkzFp0iTY2dmhZcuWQkcjIqISxDEwVCo2b94MfX39Yr9q1apVaq+bm5uL77//HrVq1UKPHj1gamqKkydPqlzZ8ykSExPf+1709fWRmJhYgu+AiIg+Bo/AVHD/dIbQzs7uH7f5Nz7//HM0bty42HX/pUz8E29v7391G4MPsbKy+uBdqq2srEr09YiI6J+xwFCpMDAwUJmUTqw0NDTg6OgodAwiInoHTyERERGR6LDAEBERkeiwwBAREZHosMAQERGR6LDAEBERkejwKqR3lMaMhx/yqbMhtmrVCvXq1UNwcHCJZQgJCcHYsWORmppaYvskIiIqbTwCQ0RERKLDAkNERESiwwIjMnl5eQgICIChoSGqVKmCqVOnKmbSffnyJXx8fGBsbAw9PT107NgRt2/fVnp+SEgIbGxsoKenhx49euD58+eKdffu3YOamhqioqKUnhMcHAxbW1sUFBR8MNvJkychk8lw5MgRuLu7Q1dXF23atMGTJ09w6NAhuLi4QC6Xo3///sjMzFQ87/Dhw2jevDmMjIxgYmKCLl26ID4+XrE+JycHAQEBsLS0hI6ODmxtbTF//nwAb2canjFjBmxsbKCtrQ0rKyuMHj36o76XycnJ6Ny5M3R1dWFvb48tW7bAzs6uRE/RERFR6WCBEZkNGzZAQ0MDkZGRWLx4MX7++WesWbMGADB48GBERUVh7969iIiIQGFhITp16oTc3FwAwPnz5+Hn54eAgADExMSgdevWmDNnjmLfdnZ28PLywvr165Vec/369Rg8eDDU1D7u4zJjxgwsXboU4eHhSEpKQp8+fRAcHIwtW7bgwIEDOHr0KH755RfF9q9fv8b48eMRFRWFsLAwqKmpoUePHorCtGTJEuzduxfbt29HXFwcNm/eDDs7OwDAH3/8gUWLFmHVqlW4ffs29uzZAzc3t4/K6ePjg0ePHuHkyZP4448/8Ouvv+LJkycf9VwiIhIWB/GKjLW1NRYtWgSZTAZnZ2dcuXIFixYtQqtWrbB3716cPXsWTZs2BfD2horW1tbYs2cPevfujcWLF6NDhw6YNGkSAKBGjRoIDw/H4cOHFfv39/fH8OHD8fPPP0NbWxsXL17ElStX8Oeff350xjlz5qBZs2YAAD8/P0yePBnx8fFwcHAAAHzxxRc4ceIEAgMDAQC9evVSev66detgamqK69evo3bt2khMTISTkxOaN28OmUwGW1tbxbaJiYmwsLCAl5cXNDU1YWNjg0aNGv1jxps3b+LYsWO4cOECGjRoAABYs2YNnJycPvp9EhGRcHgERmSaNGkCmUymeOzp6Ynbt2/j+vXr0NDQULqBoomJCZydnXHjxturnW7cuKFyg0VPT0+lx927d4e6ujp2794N4O0pp9atWyuOeHyMOnXqKP5tbm4OPT09RXkpWvbukY7bt2+jX79+cHBwgFwuV7xW0V2eBw8ejJiYGDg7O2P06NE4evSo4rm9e/dGVlYWHBwcMHToUOzevRt5eXn/mDEuLg4aGhrw8PBQLHN0dISxsfFHv08iIhIOCwwp0dLSgo+PD9avX4+cnBxs2bIFvr6+n7SPd+82LZPJVO4+LZPJlMbTdO3aFS9evMDq1atx/vx5nD9/HsDbsS8A4OHhgYSEBMyePRtZWVno06cPvvjiCwBvj0jFxcVh+fLl0NXVxTfffIOWLVsqTpsREVHFxAIjMkW/3IucO3cOTk5OcHV1RV5entL658+fIy4uDq6urgAAFxeXYp//d/7+/jh27BiWL1+OvLw89OzZsxTeiXLGKVOmoG3btnBxccHLly9VtpPL5fjyyy+xevVqbNu2DX/88QdevHgBANDV1UXXrl2xZMkSnDx5EhEREbhy5coHX9fZ2Rl5eXm4dOmSYtmdO3eKfW0iIip/OAZGZBITEzF+/Hh8/fXXuHjxIn755RcsXLgQTk5O6NatG4YOHYpVq1bBwMAA3333HapWrYpu3boBAEaPHo1mzZrhp59+Qrdu3XDkyBGl8S9FXFxc0KRJEwQGBsLX1xe6urql9n6MjY1hYmKCX3/9FZaWlkhMTMR3332ntM3PP/8MS0tLuLu7Q01NDTt27ICFhQWMjIwQEhKC/Px8NG7cGHp6evjtt9+gq6urNE6mODVr1oSXlxeGDRuGFStWQFNTE99++y10dXWVTtEREVH5xALzjk+dGVcIPj4+yMrKQqNGjaCuro4xY8Zg2LBhAN5eLTRmzBh06dIFOTk5aNmyJQ4ePKg4hdOkSROsXr0a06dPx7Rp0+Dl5YUpU6Zg9uzZKq/j5+eH8PDwTz599KnU1NSwdetWjB49GrVr14azszOWLFmCVq1aKbYxMDBAUFAQbt++DXV1dTRs2BAHDx6EmpoajIyMsGDBAowfPx75+flwc3PDvn37YGJi8o+vvXHjRvj5+aFly5awsLDA/Pnzce3aNejo6JTiOyYiopIgKyyaRKSCSU9Ph6GhIdLS0iCXy5XWvXnzBgkJCbC3t+cvq/eYPXs2duzYgcuXLwsdpcw8ePAA1tbWOHbsGNq2bSt0nP+Mn3Mi8SuNW9yU9z/WP/T7+13/aQzMggULIJPJMHbsWMWyN2/eYOTIkTAxMYG+vj569eqFx48fKz0vMTERnTt3hp6eHszMzDBx4kSVK0dOnjwJDw8PaGtrw9HRESEhIf8lKn2kjIwMXL16FUuXLsWoUaOEjlOqjh8/jr179yIhIQHh4eHo27cv7Ozs0LJlS6GjERHRP/jXBebChQtYtWqV0iWzADBu3Djs27cPO3bswKlTp/Do0SOlQaD5+fno3LkzcnJyEB4ejg0bNiAkJATTpk1TbJOQkIDOnTujdevWiImJwdixY+Hv748jR47827j0kQICAlC/fn20atVK5fTR8OHDoa+vX+zX8OHDBUpcvL/++uu9WfX19QEAubm5+P7771GrVi306NEDpqamOHnypMpVU0REVP78q1NIGRkZ8PDwwPLlyzFnzhzFHZLT0tJgamqKLVu2KC5zvXnzJlxcXBAREYEmTZrg0KFD6NKlCx49egRzc3MAwMqVKxEYGIinT59CS0sLgYGBOHDgAK5evap4zb59+yI1NbXYQafF4SmkkvfkyROkp6cXu04ul8PMzKyME71fVlYWHj58+N71jo6OZZhGGPycE4kfTyG9/xTSvxrEO3LkSHTu3BleXl5KU9FHR0cjNzcXXl5eimU1a9aEjY2NosBERETAzc1NUV4AwNvbGyNGjMC1a9fg7u6OiIgIpX0UbfPuqaq/y87ORnZ2tuLx+37R0r9nZmZWrkrKh+jq6kqipBARSdUnF5itW7fi4sWLuHDhgsq6lJQUaGlpwcjISGm5ubk5UlJSFNu8W16K1het+9A26enpyMrKKvay3vnz52PmzJmf9F4q6PhlIgD8fBNRxfZJY2CSkpIwZswYbN68udwdkp48eTLS0tIUX0lJSe/dtmiMw7t3RCaqaIo+3xzTQ0QV0ScdgYmOjsaTJ0+U7h+Tn5+P06dPY+nSpThy5AhycnKQmpqqdBTm8ePHsLCwAABYWFggMjJSab9FVym9u83fr1x6/Pgx5HL5eydV09bWhra29ke9D3V1dRgZGSnux6Onp8fJy6jCKCwsRGZmJp48eQIjIyOoq6sLHYmIqMR9UoFp27atyhTtQ4YMQc2aNREYGAhra2toamoiLCxMcYfhuLg4JCYmKm4a6Onpiblz5+LJkyeK8RShoaGQy+WKKe89PT1x8OBBpdcJDQ1VufHgf1FUlt69qSBRRWJkZKT4nBMRVTSfVGAMDAxQu3ZtpWWVKlWCiYmJYrmfnx/Gjx+PypUrQy6XY9SoUfD09ESTJk0AAO3bt4erqysGDhyIoKAgpKSkYMqUKRg5cqTiCMrw4cOxdOlSTJo0Cb6+vjh+/Di2b9+OAwcOlMR7BvD2hoKWlpYwMzPjjf+owtHU1OSRFyKq0Er8VgKLFi2CmpoaevXqhezsbHh7e2P58uWK9erq6ti/fz9GjBgBT09PVKpUCYMGDcKsWbMU29jb2+PAgQMYN24cFi9ejGrVqmHNmjXw9vYu6bhQV1fnD3oiIiKRkeStBIiIiMSA88CU0q0EiIiIiITAAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiU+I3cyQiEquSvu9Meb/nDJGY8QgMERERiQ6PwJAgpHiHVSIiKjk8AkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDRERE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", "text/plain": [ "
" ] @@ -709,13 +859,13 @@ }, { "cell_type": "code", - "execution_count": 79, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 61a0ba75-d130-445a-b412-e0b4bc0af31e is DONE. 12.9 kB processed. Open Job" + "Query job 58f9cb42-382b-428b-9ed5-95c064e3ab29 is DONE. 12.9 kB processed. Open Job" ], "text/plain": [ "" @@ -727,7 +877,7 @@ { "data": { "text/html": [ - "Query job fc75ea4f-4296-4c93-9f58-55acb67946c9 is DONE. 12.9 kB processed. Open Job" + "Query job 18128891-8a44-42bb-9f30-a1e978203f41 is DONE. 12.9 kB processed. Open Job" ], "text/plain": [ "" @@ -739,7 +889,7 @@ { "data": { "text/html": [ - "Query job d4a76bed-2722-488d-8a8a-a4301bb589fd is DONE. 12.9 kB processed. Open Job" + "Query job 1ddc23a6-4b05-448b-bb21-d778634ea5c5 is DONE. 12.9 kB processed. Open Job" ], "text/plain": [ "" @@ -751,7 +901,7 @@ { "data": { "text/html": [ - "Query job 9c8f68a5-c141-4f3d-b1e8-ca6b5964ec96 is DONE. 12.9 kB processed. Open Job" + "Query job b4617c85-24c9-4b63-8926-b5db63e7c319 is DONE. 12.9 kB processed. Open Job" ], "text/plain": [ "" @@ -763,7 +913,7 @@ { "data": { "text/html": [ - "Query job ae0a5e44-e566-453f-bd97-3c5c8532e79f is DONE. 12.9 kB processed. Open Job" + "Query job 95df92b9-1b7d-4c6d-9f20-69f928e36850 is DONE. 12.9 kB processed. Open Job" ], "text/plain": [ "" @@ -774,7 +924,7 @@ }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAyUAAAGFCAYAAADjF1xYAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAABqp0lEQVR4nO3dd1hTZ/8G8DsJe++hoogg4gBxj9aFCq5WX1utWqvVumfd/mytVm21jlbraGutWut+bau11l19FScquBARwQkiU3Ygye8PampkCDKehNyf6+LS5Jw85z4hQL55xpGoVCoViIiIiIiIBJGKDkBERERERPqNRQkREREREQnFooSIiIiIiIRiUUJEREREREKxKCEiIiIiIqFYlBARERERkVAsSoiIiIiISCgWJUREREREJBSLEiIiIiIiEopFCRERERERCcWihIiIiIiIhGJRQkREREREQrEoISIiIiIioViUEBERERGRUCxKiIiIiIhIKBYlREREREQkFIsSIiIiIiISikUJEREREREJxaKEiIiIiIiEYlFCRERERERCsSghIiIiIiKhWJQQEREREZFQLEqIiIiIiEgoFiVERERERCQUixIiIiIiIhKKRQkREREREQnFooSIiIiIiIRiUUJERELNmzcPjRs3LvH+MTExkEgkCA0NBQCcOHECEokEKSkpFZJP23To0AGTJ08uczuJiYlwcnJCTExMmdvSJRKJBL///juAgq+lyvA6x3z5e+7u7o5vvvmmXHO1atUKe/bsKdc2iUqDRQkREZWrs2fPQiaToUePHpVyvDZt2iA2NhbW1tav3camTZsgkUggkUgglUpRo0YNfPjhh4iPjy/HpOXj119/xYIFC8rczqJFi/D222/D3d0dwL9vlp9/2dvbo2vXrrhy5UqZj6Wt3NzcEBsbi4YNG4qOUioXL17EyJEjy7XNTz75BLNmzYJSqSzXdolKikUJERGVqw0bNmDChAn43//+h8ePH1f48YyMjODi4gKJRFKmdqysrBAbG4uHDx9i/fr1+OuvvzB48OBySll+7OzsYGlpWaY2MjMzsWHDBgwfPrzAtqNHjyI2NhaHDh1Ceno6unXrVmV7oWQyGVxcXGBgYCA6Sqk4OjrCzMysXNvs1q0b0tLS8Ndff5Vru0QlxaKEiIjKTXp6Onbu3IkxY8agR48e2LRpU4F9Fi9e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VSVKRX6holLmFx9SqehEREREr8SihIiIiIiIhOJHaEREREREJBSLEiIiIiIiEopFCRERERERCcWihIiIiIiIhGJRQkREREREQrEoISIiIiIioViUEBERERGRUCxKiIiIiIhIKBYlREREREQkFIsSIiIiIiISikUJEREREREJxaKEiIiIiIiEYlFCRERERERCsSghIiIiIiKhWJQQEREREZFQLEqIiIiIiEgoFiVERERERCQUixIiIiIiIhKKRQkREREREQnFooSIiIiIiIRiUUJEREREREKxKCEiIiIiIqFYlBARERERkVAsSoiIiIiISCgWJUREREREJBSLEiIiIiIiEopFCRERERERCcWihIiIiIiIhGJRQkREREREQrEoISIiIiIioViUEBERERGRUCxKiIiIiIhIKBYlREREREQkFIsSIiIiIiISikUJEREREREJxaKEiIiIiIiEYlFCRERERERCsSghIiIiIiKhWJQQEREREZFQLEqIiIiIiEgoFiVERERERCQUixIiIiIiIhKKRQkREREREQnFooSIiIiIiIRiUUJEREREREL9P6MtWHxUCA+9AAAAAElFTkSuQmCC", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAyUAAAGFCAYAAADjF1xYAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAABqp0lEQVR4nO3dd1hTZ/8G8DsJe++hoogg4gBxj9aFCq5WX1utWqvVumfd/mytVm21jlbraGutWut+bau11l19FScquBARwQkiU3Ygye8PampkCDKehNyf6+LS5Jw85z4hQL55xpGoVCoViIiIiIiIBJGKDkBERERERPqNRQkREREREQnFooSIiIiIiIRiUUJEREREREKxKCEiIiIiIqFYlBARERERkVAsSoiIiIiISCgWJUREREREJBSLEiIiIiIiEopFCRERERERCcWihIiIiIiIhGJRQkREREREQrEoISIiIiIioViUEBERERGRUCxKiIiIiIhIKBYlREREREQkFIsSIiIiIiISikUJEREREREJxaKEiIiIiIiEYlFCRERERERCsSghIiIiIiKhWJQQEREREZFQLEqIiIiIiEgoFiVERERERCQUixIiIiIiIhKKRQkREREREQnFooSIiIiIiIRiUUJERELNmzcPjRs3LvH+MTExkEgkCA0NBQCcOHECEokEKSkpFZJP23To0AGTJ08uczuJiYlwcnJCTExMmdvSJRKJBL///juAgq+lyvA6x3z5e+7u7o5vvvmmXHO1atUKe/bsKdc2iUqDRQkREZWrs2fPQiaToUePHpVyvDZt2iA2NhbW1tav3camTZsgkUggkUgglUpRo0YNfPjhh4iPjy/HpOXj119/xYIFC8rczqJFi/D222/D3d0dwL9vlp9/2dvbo2vXrrhy5UqZj6Wt3NzcEBsbi4YNG4qOUioXL17EyJEjy7XNTz75BLNmzYJSqSzXdolKikUJERGVqw0bNmDChAn43//+h8ePH1f48YyMjODi4gKJRFKmdqysrBAbG4uHDx9i/fr1+OuvvzB48OBySll+7OzsYGlpWaY2MjMzsWHDBgwfPrzAtqNHjyI2NhaHDh1Ceno6unXrVmV7oWQyGVxcXGBgYCA6Sqk4OjrCzMysXNvs1q0b0tLS8Ndff5Vru0QlxaKEiIjKTXp6Onbu3IkxY8agR48e2LRpU4F9Fi9e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VSVKRX6holLmFx9SqehEREREr8SihIiIiIiIhOJHaEREREREJBSLEiIiIiIiEopFCRERERERCcWihIiIiIiIhGJRQkREREREQrEoISIiIiIioViUEBERERGRUCxKiIiIiIhIKBYlREREREQkFIsSIiIiIiISikUJEREREREJxaKEiIiIiIiEYlFCRERERERCsSghIiIiIiKhWJQQEREREZFQLEqIiIiIiEgoFiVERERERCQUixIiIiIiIhKKRQkREREREQnFooSIiIiIiIRiUUJEREREREKxKCEiIiIiIqFYlBARERERkVAsSoiIiIiISCgWJUREREREJBSLEiIiIiIiEopFCRERERERCcWihIiIiIiIhGJRQkREREREQrEoISIiIiIioViUEBERERGRUCxKiIiIiIhIKBYlREREREQkFIsSIiIiIiISikUJEREREREJxaKEiIiIiIiEYlFCRERERERCsSghIiIiIiKhWJQQEREREZFQLEqIiIiIiEgoFiVERERERCQUixIiIiIiIhKKRQkREREREQnFooSIiIiIiIRiUUJEREREREL9P6MtWHxUCA+9AAAAAElFTkSuQmCC", "text/plain": [ "
" ] @@ -807,7 +957,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -822,7 +972,7 @@ { "data": { "text/html": [ - "Query job ff4c22ff-2735-46db-b52b-a33cf8fef27e is DONE. 28.9 kB processed. Open Job" + "Query job 6474b1c7-b6e5-4be7-93c6-cf8b0338dd58 is DONE. 34.5 kB processed. Open Job" ], "text/plain": [ "" @@ -843,7 +993,7 @@ { "data": { "text/html": [ - "Load job 49eb4aa0-7166-484e-90b1-63a675eafd00 is DONE. Open Job" + "Load job c93fdfb6-bbe1-446c-b7be-4922d67498ed is DONE. Open Job" ], "text/plain": [ "" @@ -1834,7 +1984,8 @@ "toc_visible": true }, "kernelspec": { - "display_name": "Python 3", + "display_name": "venv", + "language": "python", "name": "python3" }, "language_info": { @@ -1847,7 +1998,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.12.6" } }, "nbformat": 4, diff --git a/noxfile.py b/noxfile.py index 967ced87ab..863c7b26d3 100644 --- a/noxfile.py +++ b/noxfile.py @@ -589,20 +589,16 @@ def prerelease(session: nox.sessions.Session, tests_path, extra_pytest_options=( ) already_installed.add("pandas") - # Ibis has introduced breaking changes. Let's exclude ibis head - # from prerelease install list for now. We should enable the head back - # once bigframes supports the version at HEAD. - # session.install( - # "--upgrade", - # "-e", # Use -e so that py.typed file is included. - # "git+https://github.com/ibis-project/ibis.git#egg=ibis-framework", - # ) + # Try to avoid a cap on our SQLGlot so that bigframes + # can be integrated with SQLMesh. See: + # https://github.com/googleapis/python-bigquery-dataframes/issues/942 + # If SQLGlot introduces something that breaks us, lets file an issue + # upstream and/or make sure we fix bigframes to work with it. session.install( "--upgrade", - "--pre", - "ibis-framework>=9.0.0,<=9.2.0", + "git+https://github.com/tobymao/sqlglot.git#egg=sqlglot", ) - already_installed.add("ibis-framework") + already_installed.add("sqlglot") # Workaround https://github.com/googleapis/python-db-dtypes-pandas/issues/178 session.install("--no-deps", "db-dtypes") diff --git a/samples/snippets/create_single_timeseries_forecasting_model_test.py b/samples/snippets/create_single_timeseries_forecasting_model_test.py index b66860418b..60b8d13149 100644 --- a/samples/snippets/create_single_timeseries_forecasting_model_test.py +++ b/samples/snippets/create_single_timeseries_forecasting_model_test.py @@ -91,8 +91,22 @@ def test_create_single_timeseries() -> None: # 3 0 1 1 False -2470.632377 4945.264753 44319.379307 ['WEEKLY'] False False True # 4 2 1 1 True -2463.671247 4937.342493 42633.299513 ['WEEKLY'] False False True # [END bigquery_dataframes_single_timeseries_forecasting_model_tutorial_evaluate] + + # [START bigquery_dataframes_single_timeseries_forecasting_model_tutorial_forecast] + prediction = model.predict(horizon=30, confidence_level=0.8) + + print(prediction.peek()) + # Expected output: + # forecast_timestamp forecast_value standard_error confidence_level prediction_interval_lower_bound prediction_interval_upper_bound confidence_interval_lower_bound confidence_interval_upper_bound + # 11 2017-08-13 00:00:00+00:00 1845.439732 328.060405 0.8 1424.772257 2266.107208 1424.772257 2266.107208 + # 29 2017-08-31 00:00:00+00:00 2615.993932 431.286628 0.8 2062.960849 3169.027015 2062.960849 3169.027015 + # 7 2017-08-09 00:00:00+00:00 2639.285993 300.301186 0.8 2254.213792 3024.358193 2254.213792 3024.358193 + # 25 2017-08-27 00:00:00+00:00 1853.735689 410.596551 0.8 1327.233216 2380.238162 1327.233216 2380.238162 + # 1 2017-08-03 00:00:00+00:00 2621.33159 241.093355 0.8 2312.180802 2930.482379 2312.180802 2930.482379 + # [END bigquery_dataframes_single_timeseries_forecasting_model_tutorial_forecast] assert coef is not None assert summary is not None assert model is not None assert parsed_date is not None + assert prediction is not None assert total_visits is not None diff --git a/scripts/manage_cloud_functions.py b/scripts/manage_cloud_functions.py index 145e178f4d..ccf588bde7 100644 --- a/scripts/manage_cloud_functions.py +++ b/scripts/manage_cloud_functions.py @@ -63,7 +63,7 @@ def get_bigframes_functions(project, region): - parent = f"projects/{args.project_id}/locations/{region}" + parent = f"projects/{project}/locations/{region}" functions = GCF_CLIENT.list_functions( functions_v2.ListFunctionsRequest(parent=parent) ) @@ -72,7 +72,7 @@ def get_bigframes_functions(project, region): function for function in functions if function.name.startswith( - f"projects/{args.project_id}/locations/{region}/functions/bigframes-" + f"projects/{project}/locations/{region}/functions/bigframes-" ) ] diff --git a/setup.py b/setup.py index e3fda9d36f..74a0d5475c 100644 --- a/setup.py +++ b/setup.py @@ -68,7 +68,7 @@ "python-dateutil>=2.8.2,<3", "pytz>=2022.7", "toolz>=0.11,<2", - "typing-extensions>=4.3.0,<5", + "typing-extensions>=4.5.0,<5", "rich>=12.4.4,<14", ] extras = { diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index c0ffcfaa1c..015153cb01 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -26,3 +26,11 @@ tabulate==0.9 ipywidgets==7.7.1 humanize==4.6.0 matplotlib==3.7.1 +# For vendored ibis-framework. +atpublic==2.3 +parsy==2.0 +python-dateutil==2.8.2 +pytz==2022.7 +toolz==0.11 +typing-extensions==4.5.0 +rich==12.4.4 diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 4368a6511d..d0eb6c1904 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -24,6 +24,7 @@ import google.api_core.exceptions from google.cloud import bigquery, functions_v2, storage import pandas +import pyarrow import pytest import test_utils.prefixer @@ -261,6 +262,43 @@ def func(x, y): ) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_binop_array_output( + session, scalars_dfs, dataset_id, bq_cf_connection +): + try: + + def func(x, y): + return [len(x), abs(y % 4)] + + remote_func = session.remote_function( + [str, int], + list[int], + dataset_id, + bq_cf_connection, + reuse=False, + )(func) + + scalars_df, scalars_pandas_df = scalars_dfs + + scalars_df = scalars_df.dropna() + scalars_pandas_df = scalars_pandas_df.dropna() + bf_result = ( + scalars_df["string_col"] + .combine(scalars_df["int64_col"], remote_func) + .to_pandas() + ) + pd_result = scalars_pandas_df["string_col"].combine( + scalars_pandas_df["int64_col"], func + ) + pandas.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, remote_func + ) + + @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_decorator_with_bigframes_series( session, scalars_dfs, dataset_id, bq_cf_connection @@ -2117,6 +2155,90 @@ def foo(x, y, z): ) +def test_df_apply_axis_1_multiple_params_array_output(session): + bf_df = bigframes.dataframe.DataFrame( + { + "Id": [1, 2, 3], + "Age": [22.5, 23, 23.5], + "Name": ["alpha", "beta", "gamma"], + } + ) + + expected_dtypes = ( + bigframes.dtypes.INT_DTYPE, + bigframes.dtypes.FLOAT_DTYPE, + bigframes.dtypes.STRING_DTYPE, + ) + + # Assert the dataframe dtypes + assert tuple(bf_df.dtypes) == expected_dtypes + + try: + + @session.remote_function([int, float, str], list[str], reuse=False) + def foo(x, y, z): + return [str(x), str(y), z] + + assert getattr(foo, "is_row_processor") is False + assert getattr(foo, "input_dtypes") == expected_dtypes + assert getattr(foo, "output_dtype") == pandas.ArrowDtype( + pyarrow.list_( + bigframes.dtypes.bigframes_dtype_to_arrow_dtype( + bigframes.dtypes.STRING_DTYPE + ) + ) + ) + + # Fails to apply on dataframe with incompatible number of columns + with pytest.raises( + ValueError, + match="^Remote function takes 3 arguments but DataFrame has 2 columns\\.$", + ): + bf_df[["Id", "Age"]].apply(foo, axis=1) + with pytest.raises( + ValueError, + match="^Remote function takes 3 arguments but DataFrame has 4 columns\\.$", + ): + bf_df.assign(Country="lalaland").apply(foo, axis=1) + + # Fails to apply on dataframe with incompatible column datatypes + with pytest.raises( + ValueError, + match="^Remote function takes arguments of types .* but DataFrame dtypes are .*", + ): + bf_df.assign(Age=bf_df["Age"].astype("Int64")).apply(foo, axis=1) + + # Successfully applies to dataframe with matching number of columns + # and their datatypes + bf_result = bf_df.apply(foo, axis=1).to_pandas() + + # Since this scenario is not pandas-like, let's handcraft the + # expected result + expected_result = pandas.Series( + [ + ["1", "22.5", "alpha"], + ["2", "23", "beta"], + ["3", "23.5", "gamma"], + ] + ) + + pandas.testing.assert_series_equal( + expected_result, bf_result, check_dtype=False, check_index_type=False + ) + + # Let's make sure the read_gbq_function path works for this function + foo_reuse = session.read_gbq_function(foo.bigframes_remote_function) + bf_result = bf_df.apply(foo_reuse, axis=1).to_pandas() + pandas.testing.assert_series_equal( + expected_result, bf_result, check_dtype=False, check_index_type=False + ) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, foo + ) + + def test_df_apply_axis_1_single_param_non_series(session): bf_df = bigframes.dataframe.DataFrame( { @@ -2181,6 +2303,46 @@ def foo(x): ) +@pytest.mark.flaky(retries=2, delay=120) +def test_df_apply_axis_1_array_output(session, scalars_dfs): + columns = ["int64_col", "int64_too"] + scalars_df, scalars_pandas_df = scalars_dfs + try: + + @session.remote_function(reuse=False) + def generate_stats(row: pandas.Series) -> list[int]: + import pandas as pd + + sum = row["int64_too"] + avg = row["int64_too"] + if pd.notna(row["int64_col"]): + sum += row["int64_col"] + avg = round((avg + row["int64_col"]) / 2) + return [sum, avg] + + assert getattr(generate_stats, "is_row_processor") + + bf_result = scalars_df[columns].apply(generate_stats, axis=1).to_pandas() + pd_result = scalars_pandas_df[columns].apply(generate_stats, axis=1) + + # bf_result.dtype is 'list[pyarrow]' while pd_result.dtype + # is 'object', ignore this mismatch by using check_dtype=False. + pandas.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + + # Let's make sure the read_gbq_function path works for this function + generate_stats_reuse = session.read_gbq_function( + generate_stats.bigframes_remote_function, + is_row_processor=True, + ) + bf_result = scalars_df[columns].apply(generate_stats_reuse, axis=1).to_pandas() + pandas.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, generate_stats + ) + + @pytest.mark.parametrize( ("ingress_settings_args", "effective_ingress_settings"), [ @@ -2397,3 +2559,125 @@ def add_one(x: int) -> int: cleanup_remote_function_assets( session.bqclient, session.cloudfunctionsclient, add_one_remote_persist ) + + +@pytest.mark.parametrize( + "array_dtype", + [ + bool, + int, + float, + str, + ], +) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_array_output( + session, scalars_dfs, dataset_id, bq_cf_connection, array_dtype +): + try: + + @session.remote_function( + dataset=dataset_id, + bigquery_connection=bq_cf_connection, + reuse=False, + ) + def featurize(x: int) -> list[array_dtype]: # type: ignore + return [array_dtype(i) for i in [x, x + 1, x + 2]] + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_too"] + bf_result = bf_int64_col.apply(featurize).to_pandas() + + pd_int64_col = scalars_pandas_df["int64_too"] + pd_result = pd_int64_col.apply(featurize) + + # ignore any dtype disparity + pandas.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + + # Let's make sure the read_gbq_function path works for this function + featurize_reuse = session.read_gbq_function( + featurize.bigframes_remote_function # type: ignore + ) + bf_result = scalars_df["int64_too"].apply(featurize_reuse).to_pandas() + pandas.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, featurize + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_array_output_partial_ordering_mode( + unordered_session, scalars_dfs, dataset_id, bq_cf_connection +): + try: + + @unordered_session.remote_function( + dataset=dataset_id, + bigquery_connection=bq_cf_connection, + reuse=False, + ) + def featurize(x: float) -> list[float]: # type: ignore + return [x, x + 1, x + 2] + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["float64_col"].dropna() + bf_result = bf_int64_col.apply(featurize).to_pandas() + + pd_int64_col = scalars_pandas_df["float64_col"].dropna() + pd_result = pd_int64_col.apply(featurize) + + # ignore any dtype disparity + pandas.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + + # Let's make sure the read_gbq_function path works for this function + featurize_reuse = unordered_session.read_gbq_function( + featurize.bigframes_remote_function # type: ignore + ) + bf_int64_col = scalars_df["float64_col"].dropna() + bf_result = bf_int64_col.apply(featurize_reuse).to_pandas() + pandas.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + unordered_session.bqclient, + unordered_session.cloudfunctionsclient, + featurize, + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_array_output_multiindex( + session, scalars_dfs, dataset_id, bq_cf_connection +): + try: + + @session.remote_function( + dataset=dataset_id, + bigquery_connection=bq_cf_connection, + reuse=False, + ) + def featurize(x: int) -> list[float]: + return [x, x + 0.5, x + 0.33] + + scalars_df, scalars_pandas_df = scalars_dfs + multiindex_cols = ["rowindex", "string_col"] + scalars_df = scalars_df.reset_index().set_index(multiindex_cols) + scalars_pandas_df = scalars_pandas_df.reset_index().set_index(multiindex_cols) + + bf_int64_col = scalars_df["int64_too"] + bf_result = bf_int64_col.apply(featurize).to_pandas() + + pd_int64_col = scalars_pandas_df["int64_too"] + pd_result = pd_int64_col.apply(featurize) + + # ignore any dtype disparity + pandas.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, featurize + ) diff --git a/tests/system/small/bigquery/test_datetime.py b/tests/system/small/bigquery/test_datetime.py new file mode 100644 index 0000000000..984e75ac10 --- /dev/null +++ b/tests/system/small/bigquery/test_datetime.py @@ -0,0 +1,66 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing + +import pandas as pd + +from bigframes import bigquery + + +def test_unix_seconds(scalars_dfs): + bigframes_df, pandas_df = scalars_dfs + + actual_res = bigquery.unix_seconds(bigframes_df["timestamp_col"]).to_pandas() + + expected_res = ( + pandas_df["timestamp_col"] + .apply(lambda ts: _to_unix_epoch(ts, "s")) + .astype("Int64") + ) + pd.testing.assert_series_equal(actual_res, expected_res) + + +def test_unix_millis(scalars_dfs): + bigframes_df, pandas_df = scalars_dfs + + actual_res = bigquery.unix_millis(bigframes_df["timestamp_col"]).to_pandas() + + expected_res = ( + pandas_df["timestamp_col"] + .apply(lambda ts: _to_unix_epoch(ts, "ms")) + .astype("Int64") + ) + pd.testing.assert_series_equal(actual_res, expected_res) + + +def test_unix_micros(scalars_dfs): + bigframes_df, pandas_df = scalars_dfs + + actual_res = bigquery.unix_micros(bigframes_df["timestamp_col"]).to_pandas() + + expected_res = ( + pandas_df["timestamp_col"] + .apply(lambda ts: _to_unix_epoch(ts, "us")) + .astype("Int64") + ) + pd.testing.assert_series_equal(actual_res, expected_res) + + +def _to_unix_epoch( + ts: pd.Timestamp, unit: typing.Literal["s", "ms", "us"] +) -> typing.Optional[int]: + if pd.isna(ts): + return None + return (ts - pd.Timestamp("1970-01-01", tz="UTC")) // pd.Timedelta(1, unit) diff --git a/tests/system/small/bigquery/test_sql.py b/tests/system/small/bigquery/test_sql.py new file mode 100644 index 0000000000..283624100a --- /dev/null +++ b/tests/system/small/bigquery/test_sql.py @@ -0,0 +1,50 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bigframes.bigquery + + +def test_sql_scalar_on_scalars_null_index(scalars_df_null_index): + series = bigframes.bigquery.sql_scalar( + """ + CAST({0} AS INT64) + + BYTE_LENGTH({1}) + + UNIX_DATE({2}) + + EXTRACT(YEAR FROM {3}) + + ST_NUMPOINTS({4}) + + LEAST( + {5}, + CAST({6} AS INT64), + CAST({7} AS INT64) + ) + CHAR_LENGTH({8}) + + EXTRACT(SECOND FROM {9}) + + UNIX_SECONDS({10}) + """, + columns=[ + # Try to include all scalar types in a single test. + scalars_df_null_index["bool_col"], + scalars_df_null_index["bytes_col"], + scalars_df_null_index["date_col"], + scalars_df_null_index["datetime_col"], + scalars_df_null_index["geography_col"], + scalars_df_null_index["int64_col"], + scalars_df_null_index["numeric_col"], + scalars_df_null_index["float64_col"], + scalars_df_null_index["string_col"], + scalars_df_null_index["time_col"], + scalars_df_null_index["timestamp_col"], + ], + ) + result = series.to_pandas() + assert len(result) == len(scalars_df_null_index) diff --git a/tests/system/small/bigquery/test_vector_search.py b/tests/system/small/bigquery/test_vector_search.py index b6a6d59c4c..6297d729ea 100644 --- a/tests/system/small/bigquery/test_vector_search.py +++ b/tests/system/small/bigquery/test_vector_search.py @@ -23,6 +23,7 @@ import bigframes.bigquery as bbq import bigframes.pandas as bpd +from tests.system.utils import assert_pandas_df_equal # Need at least 5,000 rows to create a vector index. VECTOR_DF = pd.DataFrame( @@ -148,8 +149,11 @@ def test_vector_search_basic_params_with_df(): }, index=pd.Index([1, 0, 0, 1], dtype="Int64"), ) - pd.testing.assert_frame_equal( - vector_search_result, expected, check_dtype=False, rtol=0.1 + assert_pandas_df_equal( + expected.sort_values("id"), + vector_search_result.sort_values("id"), + check_dtype=False, + rtol=0.1, ) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 4e0e5c2739..e7d6ad67e1 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2554,6 +2554,27 @@ def test_join_param_on(scalars_dfs, how): assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) +@all_joins +def test_df_join_series(scalars_dfs, how): + bf_df, pd_df = scalars_dfs + + bf_df_a = bf_df[["string_col", "int64_col", "rowindex_2"]] + bf_df_a = bf_df_a.assign(rowindex_2=bf_df_a["rowindex_2"] + 2) + bf_series_b = bf_df["float64_col"] + + if how == "cross": + with pytest.raises(ValueError): + bf_df_a.join(bf_series_b, on="rowindex_2", how=how) + else: + bf_result = bf_df_a.join(bf_series_b, on="rowindex_2", how=how).to_pandas() + + pd_df_a = pd_df[["string_col", "int64_col", "rowindex_2"]] + pd_df_a = pd_df_a.assign(rowindex_2=pd_df_a["rowindex_2"] + 2) + pd_series_b = pd_df["float64_col"] + pd_result = pd_df_a.join(pd_series_b, on="rowindex_2", how=how) + assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) + + @pytest.mark.parametrize( ("by", "ascending", "na_position"), [ @@ -2581,6 +2602,11 @@ def test_dataframe_sort_values( ) +def test_dataframe_sort_values_invalid_input(scalars_df_index): + with pytest.raises(KeyError): + scalars_df_index.sort_values(by=scalars_df_index["int64_col"]) + + def test_dataframe_sort_values_stable(scalars_df_index, scalars_pandas_df_index): bf_result = ( scalars_df_index.sort_values("int64_col", kind="stable") diff --git a/tests/system/small/test_null_index.py b/tests/system/small/test_null_index.py index f70e16447a..6da4c6ff9c 100644 --- a/tests/system/small/test_null_index.py +++ b/tests/system/small/test_null_index.py @@ -247,6 +247,7 @@ def test_null_index_stack(scalars_df_null_index, scalars_pandas_df_default_index .droplevel(level=0, axis=0) ) pd_result.index = pd_result.index.astype(bf_result.index.dtype) + pd.testing.assert_series_equal( bf_result, pd_result, diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 2f677bebed..c3f3890459 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -20,6 +20,7 @@ import pandas as pd import pyarrow import pytest +import test_utils.prefixer import bigframes import bigframes.dtypes @@ -28,6 +29,8 @@ from bigframes.functions import remote_function as rf from tests.system.utils import assert_pandas_df_equal +_prefixer = test_utils.prefixer.Prefixer("bigframes", "") + @pytest.fixture(scope="module") def bq_cf_connection() -> str: @@ -770,7 +773,7 @@ def test_read_gbq_function_reads_udfs(session, bigquery_client, dataset_id): @pytest.mark.flaky(retries=2, delay=120) -def test_read_gbq_function_enforces_explicit_types( +def test_read_gbq_function_requires_explicit_types( session, bigquery_client, dataset_id ): dataset_ref = bigquery.DatasetReference.from_string(dataset_id) @@ -839,6 +842,156 @@ def test_read_gbq_function_enforces_explicit_types( ) +@pytest.mark.parametrize( + ("session_fixture",), + [ + pytest.param("session"), + pytest.param("unordered_session"), + ], +) +@pytest.mark.parametrize( + ("array_type", "expected_data"), + [ + pytest.param(None, ["[1,2,3]", "[10,11,12]", "[100,101,102]"], id="None"), + pytest.param( + list[str], + [["1", "2", "3"], ["10", "11", "12"], ["100", "101", "102"]], + id="list-str", + ), + pytest.param( + list[int], [[1, 2, 3], [10, 11, 12], [100, 101, 102]], id="list-int" + ), + ], +) +@pytest.mark.flaky(retries=2, delay=120) +def test_read_gbq_function_respects_python_output_type( + request, session_fixture, bigquery_client, dataset_id, array_type, expected_data +): + session = request.getfixturevalue(session_fixture) + dataset_ref = bigquery.DatasetReference.from_string(dataset_id) + arg = bigquery.RoutineArgument( + name="x", + data_type=bigquery.StandardSqlDataType(bigquery.StandardSqlTypeNames.INT64), + ) + sql_routine = bigquery.Routine( + dataset_ref.routine(_prefixer.create_prefix()), + body="TO_JSON_STRING([x, x+1, x+2])", + arguments=[arg], + return_type=bigquery.StandardSqlDataType(bigquery.StandardSqlTypeNames.STRING), + description=rf_utils.get_bigframes_metadata(python_output_type=array_type), + type_=bigquery.RoutineType.SCALAR_FUNCTION, + ) + + # Create the routine in BigQuery and read it back using read_gbq_function. + bigquery_client.create_routine(sql_routine, exists_ok=True) + func = rf.read_gbq_function(str(sql_routine.reference), session=session) + + # test that the function works as expected + s = bigframes.series.Series([1, 10, 100]) + expected = pd.Series(expected_data) + actual = s.apply(func).to_pandas() + + # ignore type disparities, e.g. "int64" in pandas v/s "Int64" in bigframes + pd.testing.assert_series_equal( + expected, actual, check_dtype=False, check_index_type=False + ) + + +@pytest.mark.parametrize( + ("array_type",), + [ + pytest.param(list[bool], id="list-bool"), + pytest.param(list[float], id="list-float"), + pytest.param(list[int], id="list-int"), + pytest.param(list[str], id="list-str"), + ], +) +@pytest.mark.flaky(retries=2, delay=120) +def test_read_gbq_function_supports_python_output_type_only_for_string_outputs( + session, bigquery_client, dataset_id, array_type +): + dataset_ref = bigquery.DatasetReference.from_string(dataset_id) + arg = bigquery.RoutineArgument( + name="x", + data_type=bigquery.StandardSqlDataType(bigquery.StandardSqlTypeNames.INT64), + ) + sql_routine = bigquery.Routine( + dataset_ref.routine(_prefixer.create_prefix()), + body="x+1", + arguments=[arg], + return_type=bigquery.StandardSqlDataType(bigquery.StandardSqlTypeNames.INT64), + description=rf_utils.get_bigframes_metadata(python_output_type=array_type), + type_=bigquery.RoutineType.SCALAR_FUNCTION, + ) + + # Create the routine in BigQuery and read it back using read_gbq_function. + bigquery_client.create_routine(sql_routine, exists_ok=True) + + # reading back will fail because we currently allow specifying an explicit + # output_type for BQ functions with STRING output + with pytest.raises( + TypeError, + match="An explicit output_type should be provided only for a BigQuery function with STRING output.", + ): + rf.read_gbq_function(str(sql_routine.reference), session=session) + + +@pytest.mark.parametrize( + ("array_type",), + [ + pytest.param(list[bool], id="list-bool"), + pytest.param(list[float], id="list-float"), + pytest.param(list[int], id="list-int"), + pytest.param(list[str], id="list-str"), + ], +) +@pytest.mark.flaky(retries=2, delay=120) +def test_read_gbq_function_supported_python_output_type( + session, bigquery_client, dataset_id, array_type +): + dataset_ref = bigquery.DatasetReference.from_string(dataset_id) + arg = bigquery.RoutineArgument( + name="x", + data_type=bigquery.StandardSqlDataType(bigquery.StandardSqlTypeNames.INT64), + ) + sql_routine = bigquery.Routine( + dataset_ref.routine(_prefixer.create_prefix()), + body="CAST(x AS STRING)", + arguments=[arg], + return_type=bigquery.StandardSqlDataType(bigquery.StandardSqlTypeNames.STRING), + description=rf_utils.get_bigframes_metadata(python_output_type=array_type), + type_=bigquery.RoutineType.SCALAR_FUNCTION, + ) + + # Create the routine in BigQuery and read it back using read_gbq_function. + bigquery_client.create_routine(sql_routine, exists_ok=True) + rf.read_gbq_function(str(sql_routine.reference), session=session) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_df_apply_scalar_func(session, scalars_dfs): + scalars_df, _ = scalars_dfs + bdf = bigframes.pandas.DataFrame( + { + "Column1": scalars_df["string_col"], + "Column2": scalars_df["string_col"], + } + ) + + # The "cw_lower_case_ascii_only" is a scalar function. + func_ref = session.read_gbq_function("bqutil.fn.cw_lower_case_ascii_only") + + # DataFrame '.apply()' only supports series level application. + with pytest.raises(NotImplementedError) as context: + bdf.apply(func_ref) + assert str(context.value) == ( + "BigFrames DataFrame '.apply()' does not support remote function for " + "column-wise (i.e. with axis=0) operations, please use a regular python " + "function instead. For element-wise operations of the remote function, " + "please use '.map()'." + ) + + @pytest.mark.flaky(retries=2, delay=120) def test_read_gbq_function_multiple_inputs_not_a_row_processor(session): with pytest.raises(ValueError) as context: diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 960e40465b..e95509e033 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -407,9 +407,12 @@ def test_read_gbq_w_ambigous_name( session: bigframes.Session, ): # Ensure read_gbq works when table and column share a name - df = session.read_gbq( - "bigframes-dev.bigframes_tests_sys.ambiguous_name" - ).to_pandas() + df = ( + session.read_gbq("bigframes-dev.bigframes_tests_sys.ambiguous_name") + .sort_values("x", ascending=False) + .reset_index(drop=True) + .to_pandas() + ) pd_df = pd.DataFrame({"x": [2, 1], "ambiguous_name": [20, 10]}) pd.testing.assert_frame_equal(df, pd_df, check_dtype=False, check_index_type=False) diff --git a/tests/unit/core/test_dtypes.py b/tests/unit/core/test_dtypes.py index a5b0889bf9..e1fac624d7 100644 --- a/tests/unit/core/test_dtypes.py +++ b/tests/unit/core/test_dtypes.py @@ -259,11 +259,11 @@ def test_literal_to_ibis_scalar_throws_on_incompatible_literal(): def test_remote_function_io_types_are_supported_bigframes_types(): from bigframes_vendored.ibis.expr.datatypes.core import ( - dtype as python_type_to_bigquery_type, + dtype as python_type_to_ibis_type, ) from bigframes.dtypes import RF_SUPPORTED_IO_PYTHON_TYPES as rf_supported_io_types for python_type in rf_supported_io_types: - ibis_type = python_type_to_bigquery_type(python_type) + ibis_type = python_type_to_ibis_type(python_type) assert ibis_type in bigframes.core.compile.ibis_types.IBIS_TO_BIGFRAMES diff --git a/tests/unit/core/test_log_adapter.py b/tests/unit/core/test_log_adapter.py index 1199391813..7b626838ac 100644 --- a/tests/unit/core/test_log_adapter.py +++ b/tests/unit/core/test_log_adapter.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +from unittest import mock + +from google.cloud import bigquery import pytest from bigframes.core import log_adapter @@ -22,6 +25,12 @@ MAX_LABELS_COUNT = 56 +@pytest.fixture +def mock_bqclient(): + mock_bqclient = mock.create_autospec(spec=bigquery.Client) + return mock_bqclient + + @pytest.fixture def test_instance(): # Create a simple class for testing @@ -61,3 +70,88 @@ def test_get_and_reset_api_methods(test_instance): previous_methods = log_adapter.get_and_reset_api_methods() assert previous_methods is not None assert log_adapter._api_methods == [] + + +@pytest.mark.parametrize( + ("class_name", "method_name", "args", "kwargs", "task", "expected_labels"), + ( + ( + "DataFrame", + "resample", + ["a", "b", "c"], + {"aa": "bb", "rule": "1s"}, + log_adapter.PANDAS_API_TRACKING_TASK, + { + "task": log_adapter.PANDAS_API_TRACKING_TASK, + "class_name": "dataframe", + "method_name": "resample", + "args_count": 3, + "kwargs_0": "rule", + }, + ), + ( + "Series", + "resample", + [], + {"aa": "bb", "rule": "1s"}, + log_adapter.PANDAS_PARAM_TRACKING_TASK, + { + "task": log_adapter.PANDAS_PARAM_TRACKING_TASK, + "class_name": "series", + "method_name": "resample", + "args_count": 0, + "kwargs_0": "rule", + }, + ), + ( + "DataFrame", + "resample", + [], + {"aa": "bb"}, + log_adapter.PANDAS_API_TRACKING_TASK, + { + "task": log_adapter.PANDAS_API_TRACKING_TASK, + "class_name": "dataframe", + "method_name": "resample", + "args_count": 0, + }, + ), + ( + "DataFrame", + "resample", + [], + {}, + log_adapter.PANDAS_API_TRACKING_TASK, + { + "task": log_adapter.PANDAS_API_TRACKING_TASK, + "class_name": "dataframe", + "method_name": "resample", + "args_count": 0, + }, + ), + ), +) +def test_submit_pandas_labels( + mock_bqclient, class_name, method_name, args, kwargs, task, expected_labels +): + log_adapter.submit_pandas_labels( + mock_bqclient, class_name, method_name, args, kwargs, task + ) + + mock_bqclient.query.assert_called_once() + + query_call_args = mock_bqclient.query.call_args_list[0] + labels = query_call_args[1]["job_config"].labels + assert labels == expected_labels + + +def test_submit_pandas_labels_without_valid_params_for_param_logging(mock_bqclient): + log_adapter.submit_pandas_labels( + mock_bqclient, + "Series", + "resample", + task=log_adapter.PANDAS_PARAM_TRACKING_TASK, + ) + + # For param tracking task without kwargs, we won't submit labels + mock_bqclient.query.assert_not_called() diff --git a/tests/unit/core/test_sql.py b/tests/unit/core/test_sql.py index a2ee2f359e..ca286cafff 100644 --- a/tests/unit/core/test_sql.py +++ b/tests/unit/core/test_sql.py @@ -12,10 +12,66 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime +import decimal + +import pytest +import shapely # type: ignore from bigframes.core import sql +@pytest.mark.parametrize( + ("value", "expected"), + ( + # Try to have some literals for each scalar data type: + # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types + (None, "NULL"), + # TODO: support ARRAY type (possibly another method?) + (True, "True"), + (False, "False"), + ( + b"\x01\x02\x03ABC", + r"b'\x01\x02\x03ABC'", + ), + ( + datetime.date(2025, 1, 1), + "DATE('2025-01-01')", + ), + ( + datetime.datetime(2025, 1, 2, 3, 45, 6, 789123), + "DATETIME('2025-01-02T03:45:06.789123')", + ), + ( + shapely.Point(0, 1), + "ST_GEOGFROMTEXT('POINT (0 1)')", + ), + # TODO: INTERVAL type (e.g. from dateutil.relativedelta) + # TODO: JSON type (TBD what Python object that would correspond to) + (123, "123"), + (decimal.Decimal("123.75"), "CAST('123.75' AS NUMERIC)"), + # TODO: support BIGNUMERIC by looking at precision/scale of the DECIMAL + (123.75, "123.75"), + # TODO: support RANGE type + ("abc", "'abc'"), + # TODO: support STRUCT type (possibly another method?) + ( + datetime.time(12, 34, 56, 789123), + "TIME(DATETIME('1970-01-01 12:34:56.789123'))", + ), + ( + datetime.datetime( + 2025, 1, 2, 3, 45, 6, 789123, tzinfo=datetime.timezone.utc + ), + "TIMESTAMP('2025-01-02T03:45:06.789123+00:00')", + ), + ), +) +def test_simple_literal(value, expected): + got = sql.simple_literal(value) + assert got == expected + + def test_create_vector_search_sql_simple(): result_query = sql.create_vector_search_sql( sql_string="SELECT embedding FROM my_embeddings_table WHERE id = 1", diff --git a/tests/unit/functions/test_remote_function_utils.py b/tests/unit/functions/test_remote_function_utils.py new file mode 100644 index 0000000000..0bcfee5c4e --- /dev/null +++ b/tests/unit/functions/test_remote_function_utils.py @@ -0,0 +1,138 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import bigframes.dtypes +from bigframes.functions import _utils + + +@pytest.mark.parametrize( + ["metadata_options", "metadata_string"], + ( + pytest.param( + {}, + '{"value": {}}', + id="empty", + ), + pytest.param( + {"python_output_type": None}, + '{"value": {}}', + id="None", + ), + pytest.param( + {"python_output_type": list[bool]}, + '{"value": {"python_array_output_type": "bool"}}', + id="list-bool", + ), + pytest.param( + {"python_output_type": list[float]}, + '{"value": {"python_array_output_type": "float"}}', + id="list-float", + ), + pytest.param( + {"python_output_type": list[int]}, + '{"value": {"python_array_output_type": "int"}}', + id="list-int", + ), + pytest.param( + {"python_output_type": list[str]}, + '{"value": {"python_array_output_type": "str"}}', + id="list-str", + ), + ), +) +def test_get_bigframes_metadata(metadata_options, metadata_string): + assert _utils.get_bigframes_metadata(**metadata_options) == metadata_string + + +@pytest.mark.parametrize( + ["output_type"], + ( + pytest.param(bool), + pytest.param(bytes), + pytest.param(float), + pytest.param(int), + pytest.param(str), + pytest.param(list), + pytest.param(list[bytes], id="list-bytes"), + ), +) +def test_get_bigframes_metadata_array_type_not_serializable(output_type): + with pytest.raises(ValueError) as context: + _utils.get_bigframes_metadata(python_output_type=output_type) + assert str(context.value) == ( + f"python_output_type {output_type} is not serializable." + ) + + +@pytest.mark.parametrize( + ["metadata_string", "python_output_type"], + ( + pytest.param( + None, + None, + id="None", + ), + pytest.param( + "", + None, + id="empty", + ), + pytest.param( + "{}", + None, + id="empty-dict", + ), + pytest.param( + '{"value": {}}', + None, + id="empty-value", + ), + pytest.param( + '{"value": {"python_array_output_type": "bool"}}', + list[bool], + id="list-bool", + ), + pytest.param( + '{"value": {"python_array_output_type": "float"}}', + list[float], + id="list-float", + ), + pytest.param( + '{"value": {"python_array_output_type": "int"}}', + list[int], + id="list-int", + ), + pytest.param( + '{"value": {"python_array_output_type": "str"}}', + list[str], + id="list-str", + ), + ), +) +def test_get_python_output_type_from_bigframes_metadata( + metadata_string, python_output_type +): + assert ( + _utils.get_python_output_type_from_bigframes_metadata(metadata_string) + == python_output_type + ) + + +def test_metadata_roundtrip_supported_array_types(): + for array_of in bigframes.dtypes.RF_SUPPORTED_ARRAY_OUTPUT_PYTHON_TYPES: + ser = _utils.get_bigframes_metadata(python_output_type=list[array_of]) # type: ignore + deser = _utils.get_python_output_type_from_bigframes_metadata(ser) + assert deser == list[array_of] # type: ignore diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index 36caea0c0e..fa05fffcb2 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -341,10 +341,12 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) (("string_col", "in", ["Hello, World!", "こんにちは"]),), ], 123, # max_results, - datetime.datetime(2024, 5, 14, 12, 42, 36, 125125), + datetime.datetime( + 2024, 5, 14, 12, 42, 36, 125125, tzinfo=datetime.timezone.utc + ), ( "SELECT `row_index`, `string_col` FROM `test_table` " - "FOR SYSTEM_TIME AS OF TIMESTAMP('2024-05-14T12:42:36.125125') " + "FOR SYSTEM_TIME AS OF TIMESTAMP('2024-05-14T12:42:36.125125+00:00') " "WHERE `rowindex` NOT IN (0, 6) OR `string_col` IN ('Hello, World!', " "'こんにちは') LIMIT 123" ), @@ -364,14 +366,16 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) ("string_col", "==", "Hello, World!"), ], 123, # max_results, - datetime.datetime(2024, 5, 14, 12, 42, 36, 125125), + datetime.datetime( + 2024, 5, 14, 12, 42, 36, 125125, tzinfo=datetime.timezone.utc + ), ( """SELECT `rowindex`, `string_col` FROM (SELECT rowindex, string_col, FROM `test_table` AS t ) """ - "FOR SYSTEM_TIME AS OF TIMESTAMP('2024-05-14T12:42:36.125125') " + "FOR SYSTEM_TIME AS OF TIMESTAMP('2024-05-14T12:42:36.125125+00:00') " "WHERE `rowindex` < 4 AND `string_col` = 'Hello, World!' " "LIMIT 123" ), diff --git a/third_party/bigframes_vendored/ibis/backends/sql/compilers/base.py b/third_party/bigframes_vendored/ibis/backends/sql/compilers/base.py index a9f9fdef9c..ccd4a57e11 100644 --- a/third_party/bigframes_vendored/ibis/backends/sql/compilers/base.py +++ b/third_party/bigframes_vendored/ibis/backends/sql/compilers/base.py @@ -1089,6 +1089,13 @@ def visit_SimpleCase(self, op, *, base=None, cases, results, default): visit_SearchedCase = visit_SimpleCase + def visit_SqlScalar(self, op, *, sql_template, values, output_type): + # TODO: can we include a string in the sqlglot expression without parsing? + return sg.parse_one( + sql_template.format(*[value.sql(dialect="bigquery") for value in values]), + dialect="bigquery", + ) + def visit_ExistsSubquery(self, op, *, rel): select = rel.this.select(1, append=False) return self.f.exists(select) diff --git a/third_party/bigframes_vendored/ibis/expr/operations/generic.py b/third_party/bigframes_vendored/ibis/expr/operations/generic.py index 36836b5963..c77ecc3e71 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/generic.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/generic.py @@ -330,4 +330,19 @@ def dtype(self): return rlz.highest_precedence_dtype(exprs) +@public +class SqlScalar(Value): + """Inject a SQL string as a scalar value.""" + + sql_template: str + values: VarTuple[Value] + output_type: dt.DataType + + shape = ds.scalar + + @property + def dtype(self): + return self.output_type + + public(NULL=NULL) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index f1565ed536..c8ca1b74b5 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -4384,7 +4384,7 @@ def join(self, other, *, on: Optional[str] = None, how: str) -> DataFrame: Args: other: - DataFrame with an Index similar to the Index of this one. + DataFrame or Series with an Index similar to the Index of this one. on: Column in the caller to join on the index in other, otherwise joins index-on-index. Like an Excel VLOOKUP operation. @@ -4626,6 +4626,33 @@ def apply(self, func, *, axis=0, args=(), **kwargs): 1 19 dtype: Int64 + You could return an array output for every input row from the remote + function. + + >>> @bpd.remote_function(reuse=False) + ... def marks_analyzer(marks: pd.Series) -> list[float]: + ... import statistics + ... average = marks.mean() + ... median = marks.median() + ... gemetric_mean = statistics.geometric_mean(marks.values) + ... harmonic_mean = statistics.harmonic_mean(marks.values) + ... return [ + ... round(stat, 2) for stat in + ... (average, median, gemetric_mean, harmonic_mean) + ... ] + + >>> df = bpd.DataFrame({ + ... "physics": [67, 80, 75], + ... "chemistry": [88, 56, 72], + ... "algebra": [78, 91, 79] + ... }, index=["Alice", "Bob", "Charlie"]) + >>> stats = df.apply(marks_analyzer, axis=1) + >>> stats + Alice [77.67 78. 77.19 76.71] + Bob [75.67 80. 74.15 72.56] + Charlie [75.33 75. 75.28 75.22] + dtype: list[pyarrow] + You could also apply a remote function which accepts multiple parameters to every row of a DataFrame by using it with `axis=1` if the DataFrame has matching number of columns and data types. Note: This feature is @@ -6772,9 +6799,9 @@ def iat(self): >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None + >>> df = bpd.DataFrame([[0, 2, 3], [0, 4, 1], [10, 20, 30]], ... columns=['A', 'B', 'C']) - >>> bpd.options.display.progress_bar = None >>> df A B C 0 0 2 3 @@ -6806,9 +6833,9 @@ def at(self): >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None + >>> df = bpd.DataFrame([[0, 2, 3], [0, 4, 1], [10, 20, 30]], ... index=[4, 5, 6], columns=['A', 'B', 'C']) - >>> bpd.options.display.progress_bar = None >>> df A B C 4 0 2 3 diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 7c8f452a8f..727e25836a 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -35,6 +35,51 @@ def dt(self): """ Accessor object for datetime-like properties of the Series values. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import pandas as pd + >>> bpd.options.display.progress_bar = None + + >>> seconds_series = bpd.Series(pd.date_range("2000-01-01", periods=3, freq="s")) + >>> seconds_series + 0 2000-01-01 00:00:00 + 1 2000-01-01 00:00:01 + 2 2000-01-01 00:00:02 + dtype: timestamp[us][pyarrow] + + >>> seconds_series.dt.second + 0 0 + 1 1 + 2 2 + dtype: Int64 + + >>> hours_series = bpd.Series(pd.date_range("2000-01-01", periods=3, freq="h")) + >>> hours_series + 0 2000-01-01 00:00:00 + 1 2000-01-01 01:00:00 + 2 2000-01-01 02:00:00 + dtype: timestamp[us][pyarrow] + + >>> hours_series.dt.hour + 0 0 + 1 1 + 2 2 + dtype: Int64 + + >>> quarters_series = bpd.Series(pd.date_range("2000-01-01", periods=3, freq="QE")) + >>> quarters_series + 0 2000-03-31 00:00:00 + 1 2000-06-30 00:00:00 + 2 2000-09-30 00:00:00 + dtype: timestamp[us][pyarrow] + + >>> quarters_series.dt.quarter + 0 1 + 1 2 + 2 3 + dtype: Int64 + Returns: bigframes.operations.datetimes.DatetimeMethods: An accessor containing datetime methods. @@ -97,15 +142,16 @@ def index(self): Name: Age, dtype: Int64 >>> s1.index # doctest: +ELLIPSIS MultiIndex([( 'Alice', 'Seattle'), - ( 'Bob', 'New York'), - ('Aritra', 'Kona')], - names=['Name', 'Location']) + ( 'Bob', 'New York'), + ('Aritra', 'Kona')], + names=['Name', 'Location']) >>> s1.index.values array([('Alice', 'Seattle'), ('Bob', 'New York'), ('Aritra', 'Kona')], - dtype=object) + dtype=object) Returns: - Index: The index object of the Series. + Index: + The index object of the Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -168,6 +214,13 @@ def name(self) -> Hashable: >>> s.name 'Numbers' + >>> s.name = "Integers" + >>> s + 0 1 + 1 2 + 2 3 + Name: Integers, dtype: Int64 + If the Series is part of a DataFrame: >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) @@ -182,7 +235,8 @@ def name(self) -> Hashable: 'col1' Returns: - hashable object: The name of the Series, also the column name + hashable object: + The name of the Series, also the column name if part of a DataFrame. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -260,7 +314,8 @@ def transpose(self) -> Series: dtype: string Returns: - Series: Series. + bigframes.pandas.Series: + Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -280,6 +335,7 @@ def reset_index( **Examples:** >>> import bigframes.pandas as bpd + >>> import pandas as pd >>> bpd.options.display.progress_bar = None >>> s = bpd.Series([1, 2, 3, 4], name='foo', @@ -324,6 +380,24 @@ def reset_index( 3 4 Name: foo, dtype: Int64 + >>> arrays = [np.array(['bar', 'bar', 'baz', 'baz']), + ... np.array(['one', 'two', 'one', 'two'])] + >>> s2 = bpd.Series( + ... range(4), name='foo', + ... index=pd.MultiIndex.from_arrays(arrays, + ... names=['a', 'b'])) + + If level is not set, all levels are removed from the Index. + + >>> s2.reset_index() + a b foo + 0 bar one 0 + 1 bar two 1 + 2 baz one 2 + 3 baz two 3 + + [4 rows x 3 columns] + Args: drop (bool, default False): Just reset the index, without inserting it as a column in @@ -334,7 +408,8 @@ def reset_index( when `drop` is True. Returns: - Series or DataFrame or None; When `drop` is False (the default), + bigframes.pandas.Series or bigframes.pandas.DataFrame or None: + When `drop` is False (the default), a DataFrame is returned. The newly created columns will come first in the DataFrame, followed by the original Series values. When `drop` is True, a `Series` is returned. @@ -394,8 +469,8 @@ def to_string( of rows is above `max_rows`). Returns: - str or None: String representation of Series if ``buf=None``, - otherwise None. + str or None: + String representation of Series if ``buf=None``, otherwise None. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -407,7 +482,36 @@ def to_markdown( **kwargs, ) -> str | None: """ - Print {klass} in Markdown-friendly format. + Print Series in Markdown-friendly format. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(["elk", "pig", "dog", "quetzal"], name="animal") + >>> print(s.to_markdown()) + | | animal | + |---:|:---------| + | 0 | elk | + | 1 | pig | + | 2 | dog | + | 3 | quetzal | + + Output markdown with a tabulate option. + + >>> print(s.to_markdown(tablefmt="grid")) + +----+----------+ + | | animal | + +====+==========+ + | 0 | elk | + +----+----------+ + | 1 | pig | + +----+----------+ + | 2 | dog | + +----+----------+ + | 3 | quetzal | + +----+----------+ Args: buf (str, Path or StringIO-like, optional, default None): @@ -418,7 +522,8 @@ def to_markdown( Add index (row) labels. Returns: - str: {klass} in Markdown-friendly format. + str: + Series in Markdown-friendly format. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -426,6 +531,23 @@ def to_dict(self, into: type[dict] = dict) -> Mapping: """ Convert Series to {label -> value} dict or dict-like object. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> from collections import OrderedDict, defaultdict + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 3, 4]) + >>> s.to_dict() + {np.int64(0): 1, np.int64(1): 2, np.int64(2): 3, np.int64(3): 4} + + >>> s.to_dict(into=OrderedDict) + OrderedDict({np.int64(0): 1, np.int64(1): 2, np.int64(2): 3, np.int64(3): 4}) + + >>> dd = defaultdict(list) + >>> s.to_dict(into=dd) + defaultdict(, {np.int64(0): 1, np.int64(1): 2, np.int64(2): 3, np.int64(3): 4}) + Args: into (class, default dict): The collections.abc.Mapping subclass to use as the return @@ -434,7 +556,8 @@ def to_dict(self, into: type[dict] = dict) -> Mapping: collections.defaultdict, you must pass it initialized. Returns: - collections.abc.Mapping: Key-value representation of Series. + collections.abc.Mapping: + Key-value representation of Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -445,11 +568,27 @@ def to_frame(self, name=None) -> DataFrame: The column in the new dataframe will be named name (the keyword parameter) if the name parameter is provided and not None. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(["a", "b", "c"], + ... name="vals") + >>> s.to_frame() + vals + 0 a + 1 b + 2 c + + [3 rows x 1 columns] + Args: name (Hashable, default None) Returns: - bigframes.dataframe.DataFrame: DataFrame representation of Series. + bigframes.pandas.DataFrame: + DataFrame representation of Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -491,7 +630,8 @@ def to_latex(self, buf=None, columns=None, header=True, index=True, **kwargs): Write row names (index). Returns: - str or None: If buf is None, returns the result as a string. + str or None: + If buf is None, returns the result as a string. Otherwise returns None. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -520,7 +660,8 @@ def tolist(self) -> list: [1, 2, 3] Returns: - list: list of the values + list: + list of the values. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -530,6 +671,32 @@ def to_numpy(self, dtype, copy=False, na_value=None): """ A NumPy ndarray representing the values in this Series or Index. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import pandas as pd + >>> bpd.options.display.progress_bar = None + + >>> ser = bpd.Series(pd.Categorical(['a', 'b', 'a'])) + >>> ser.to_numpy() + array(['a', 'b', 'a'], dtype=object) + + Specify the dtype to control how datetime-aware data is represented. Use + dtype=object to return an ndarray of pandas Timestamp objects, each with + the correct tz. + + >>> ser = bpd.Series(pd.date_range('2000', periods=2, tz="CET")) + >>> ser.to_numpy(dtype=object) + array([Timestamp('1999-12-31 23:00:00+0000', tz='UTC'), + Timestamp('2000-01-01 23:00:00+0000', tz='UTC')], dtype=object) + + Or ``dtype=datetime64[ns]`` to return an ndarray of native datetime64 values. + The values are converted to UTC and the timezone info is dropped. + + >>> ser.to_numpy(dtype="datetime64[ns]") + array(['1999-12-31T23:00:00.000000000', '2000-01-01T23:00:00.000000000'], + dtype='datetime64[ns]') + Args: dtype (str or numpy.dtype, optional): The dtype to pass to :meth:`numpy.asarray`. @@ -546,7 +713,8 @@ def to_numpy(self, dtype, copy=False, na_value=None): of the underlying array (for extension arrays). Returns: - numpy.ndarray: A NumPy ndarray representing the values in this + numpy.ndarray: + A NumPy ndarray representing the values in this Series or Index. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -555,11 +723,43 @@ def to_pickle(self, path, **kwargs): """ Pickle (serialize) object to file. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> original_df = bpd.DataFrame({"foo": range(5), "bar": range(5, 10)}) + >>> original_df + foo bar + 0 0 5 + 1 1 6 + 2 2 7 + 3 3 8 + 4 4 9 + + [5 rows x 2 columns] + + >>> original_df.to_pickle("./dummy.pkl") + + >>> unpickled_df = bpd.read_pickle("./dummy.pkl") + >>> unpickled_df + foo bar + 0 0 5 + 1 1 6 + 2 2 7 + 3 3 8 + 4 4 9 + + [5 rows x 2 columns] + Args: path (str, path object, or file-like object): String, path object (implementing ``os.PathLike[str]``), or file-like object implementing a binary ``write()`` function. File path where the pickled object will be stored. + + Returns: + None """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -568,7 +768,8 @@ def to_xarray(self): Return an xarray object from the pandas object. Returns: - xarray.DataArray or xarray.Dataset: Data in the pandas structure + xarray.DataArray or xarray.Dataset: + Data in the pandas structure converted to Dataset if the object is a DataFrame, or a DataArray if the object is a Series. """ @@ -606,7 +807,8 @@ def agg(self, func): function names, e.g. ``['sum', 'mean']``. Returns: - scalar or Series: Aggregated results + scalar or bigframes.pandas.Series: + Aggregated results. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -629,8 +831,8 @@ def count(self): np.int64(2) Returns: - int or Series (if level specified): Number of non-null values in the - Series. + int or bigframes.pandas.Series (if level specified): + Number of non-null values in the Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -640,8 +842,26 @@ def nunique(self) -> int: Excludes NA values by default. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 3, 5, 7, 7]) + >>> s + 0 1 + 1 3 + 2 5 + 3 7 + 4 7 + dtype: Int64 + + >>> s.nunique() + np.int64(4) + Returns: - int: number of unique elements in the object. + int: + number of unique elements in the object. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -671,6 +891,7 @@ def unique(self, keep_order=True) -> Series: Name: A, dtype: Int64 Example with order preservation: Slower, but keeps order + >>> s.unique() 0 2 1 1 @@ -678,6 +899,7 @@ def unique(self, keep_order=True) -> Series: Name: A, dtype: Int64 Example without order preservation: Faster, but loses original order + >>> s.unique(keep_order=False) 0 1 1 2 @@ -685,7 +907,8 @@ def unique(self, keep_order=True) -> Series: Name: A, dtype: Int64 Returns: - Series: The unique values returned as a Series. + bigframes.pandas.Series: + The unique values returned as a Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -697,8 +920,20 @@ def mode(self) -> Series: Always returns Series even if only one value is returned. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([2, 4, 8, 2, 4, None]) + >>> s.mode() + 0 2.0 + 1 4.0 + dtype: Float64 + Returns: - bigframes.series.Series: Modes of the Series in sorted order. + bigframes.pandas.Series: + Modes of the Series in sorted order. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -710,6 +945,53 @@ def drop_duplicates( """ Return Series with duplicate values removed. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Generate a Series with duplicated entries. + + >>> s = bpd.Series(['llama', 'cow', 'llama', 'beetle', 'llama', 'hippo'], + ... name='animal') + >>> s + 0 llama + 1 cow + 2 llama + 3 beetle + 4 llama + 5 hippo + Name: animal, dtype: string + + With the 'keep' parameter, the selection behaviour of duplicated values + can be changed. The value 'first' keeps the first occurrence for each set + of duplicated entries. The default value of keep is 'first'. + + >>> s.drop_duplicates() + 0 llama + 1 cow + 3 beetle + 5 hippo + Name: animal, dtype: string + + The value 'last' for parameter 'keep' keeps the last occurrence for + each set of duplicated entries. + + >>> s.drop_duplicates(keep='last') + 1 cow + 3 beetle + 4 llama + 5 hippo + Name: animal, dtype: string + + The value False for parameter 'keep' discards all sets of duplicated entries. + + >>> s.drop_duplicates(keep=False) + 1 cow + 3 beetle + 5 hippo + Name: animal, dtype: string + Args: keep ({'first', 'last', ``False``}, default 'first'): Method to handle dropping duplicates: @@ -719,7 +1001,8 @@ def drop_duplicates( ``False`` : Drop all duplicates. Returns: - bigframes.series.Series: Series with duplicates dropped or None if ``inplace=True``. + bigframes.pandas.Series: + Series with duplicates dropped or None if ``inplace=True``. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -731,6 +1014,54 @@ def duplicated(self, keep="first") -> Series: Series. Either all duplicates, all except the first or all except the last occurrence of duplicates can be indicated. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + By default, for each set of duplicated values, the first occurrence is + set on False and all others on True: + + >>> animals = bpd.Series(['llama', 'cow', 'llama', 'beetle', 'llama']) + >>> animals.duplicated() + 0 False + 1 False + 2 True + 3 False + 4 True + dtype: boolean + + which is equivalent to + + >>> animals.duplicated(keep='first') + 0 False + 1 False + 2 True + 3 False + 4 True + dtype: boolean + + By using 'last', the last occurrence of each set of duplicated values + is set on False and all others on True: + + >>> animals.duplicated(keep='last') + 0 True + 1 False + 2 True + 3 False + 4 False + dtype: boolean + + By setting keep on False, all duplicates are True: + + >>> animals.duplicated(keep=False) + 0 True + 1 False + 2 True + 3 False + 4 True + dtype: boolean + Args: keep ({'first', 'last', False}, default 'first'): Method to handle dropping duplicates: @@ -742,7 +1073,8 @@ def duplicated(self, keep="first") -> Series: ``False`` : Mark all duplicates as ``True``. Returns: - bigframes.series.Series: Series indicating whether each value has occurred in the + bigframes.pandas.Series: + Series indicating whether each value has occurred in the preceding values. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -754,6 +1086,23 @@ def idxmin(self) -> Hashable: If multiple values equal the minimum, the first row label with that value is returned. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(data=[1, None, 4, 1], + ... index=['A', 'B', 'C', 'D']) + >>> s + A 1.0 + B + C 4.0 + D 1.0 + dtype: Float64 + + >>> s.idxmin() + 'A' + Returns: Index: Label of the minimum value. """ @@ -766,6 +1115,24 @@ def idxmax(self) -> Hashable: If multiple values equal the maximum, the first row label with that value is returned. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(data=[1, None, 4, 3, 4], + ... index=['A', 'B', 'C', 'D', 'E']) + >>> s + A 1.0 + B + C 4.0 + D 3.0 + E 4.0 + dtype: Float64 + + >>> s.idxmax() + 'C' + Returns: Index: Label of the maximum value. """ @@ -800,7 +1167,8 @@ def round(self, decimals: int = 0) -> Series: it specifies the number of positions to the left of the decimal point. Returns: - bigframes.series.Series: Rounded values of the Series. + bigframes.pandas.Series: + Rounded values of the Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -814,6 +1182,12 @@ def explode(self, *, ignore_index: Optional[bool] = False) -> Series: >>> bpd.options.display.progress_bar = None >>> s = bpd.Series([[1, 2, 3], [], [3, 4]]) + >>> s + 0 [1 2 3] + 1 [] + 2 [3 4] + dtype: list[pyarrow] + >>> s.explode() 0 1 0 2 @@ -828,7 +1202,8 @@ def explode(self, *, ignore_index: Optional[bool] = False) -> Series: If True, the resulting index will be labeled 0, 1, …, n - 1. Returns: - bigframes.series.Series: Exploded lists to rows; index will be duplicated for these rows. + bigframes.pandas.Series: + Exploded lists to rows; index will be duplicated for these rows. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -865,7 +1240,8 @@ def corr(self, other, method="pearson", min_periods=None) -> float: are not yet supported, so a result will be returned for at least two observations. Returns: - float: Will return NaN if there are fewer than two numeric pairs, either series has a + float: + Will return NaN if there are fewer than two numeric pairs, either series has a variance or covariance of zero, or any input value is infinite. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -885,10 +1261,11 @@ def autocorr(self, lag: int = 1) -> float: >>> s = bpd.Series([0.25, 0.5, 0.2, -0.05]) >>> s.autocorr() # doctest: +ELLIPSIS np.float64(0.10355263309024067) + >>> s.autocorr(lag=2) np.float64(-1.0) - If the Pearson correlation is not well defined, then 'NaN' is returned. + If the Pearson correlation is not well defined, then 'NaN' is returned. >>> s = bpd.Series([1, 0, 0, 0]) >>> s.autocorr() @@ -899,7 +1276,8 @@ def autocorr(self, lag: int = 1) -> float: Number of lags to apply before performing autocorrelation. Returns: - float: The Pearson correlation between self and self.shift(lag). + float: + The Pearson correlation between self and self.shift(lag). """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -913,6 +1291,16 @@ def cov( The two `Series` objects are not required to be the same length and will be aligned internally before the covariance is calculated. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s1 = bpd.Series([0.90010907, 0.13484424, 0.62036035]) + >>> s2 = bpd.Series([0.12528585, 0.26962463, 0.51111198]) + >>> s1.cov(s2) + np.float64(-0.01685762652715874) + Args: other (Series): Series with which to compute the covariance. @@ -928,8 +1316,48 @@ def diff(self) -> Series: """ First discrete difference of element. - Calculates the difference of a {klass} element compared with another - element in the {klass} (default is element in previous row). + Calculates the difference of a Series element compared with another + element in the Series (default is element in previous row). + + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Difference with previous row + + >>> s = bpd.Series([1, 1, 2, 3, 5, 8]) + >>> s.diff() + 0 + 1 0 + 2 1 + 3 1 + 4 2 + 5 3 + dtype: Int64 + + Difference with 3rd previous row + + >>> s.diff(periods=3) + 0 + 1 + 2 + 3 2 + 4 4 + 5 6 + dtype: Int64 + + Difference with following row + + >>> s.diff(periods=-1) + 0 0 + 1 -1 + 2 -1 + 3 -2 + 4 -3 + 5 + dtype: Int64 Args: periods (int, default 1): @@ -937,7 +1365,8 @@ def diff(self) -> Series: values. Returns: - Series: First differences of the Series. + bigframes.pandas.Series: + First differences of the Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -977,7 +1406,8 @@ def dot(self, other) -> Series | np.ndarray: The other object to compute the dot product with its columns. Returns: - scalar, Series or numpy.ndarray: Return the dot product of the Series + scalar, bigframes.pandas.Series or numpy.ndarray: + Return the dot product of the Series and other if other is a Series, the Series of the dot product of Series and each rows of other if other is a DataFrame or a numpy.ndarray between the Series and each columns of the numpy array. @@ -1015,6 +1445,7 @@ def sort_values( **Examples:** >>> import bigframes.pandas as bpd + >>> import numpy as np >>> bpd.options.display.progress_bar = None >>> s = bpd.Series([np.nan, 1, 3, 10, 5]) @@ -1081,15 +1512,16 @@ def sort_values( ascending (bool or list of bools, default True): If True, sort values in ascending order, otherwise descending. kind (str, default to 'quicksort'): - Choice of sorting algorithm. Accepts 'quicksort’, ‘mergesort’, - ‘heapsort’, ‘stable’. Ignored except when determining whether to + Choice of sorting algorithm. Accepts quicksort', 'mergesort', + 'heapsort', 'stable'. Ignored except when determining whether to sort stably. 'mergesort' or 'stable' will result in stable reorder na_position ({'first' or 'last'}, default 'last'): Argument 'first' puts NaNs at the beginning, 'last' puts NaNs at the end. Returns: - bigframes.series.Series: Series ordered by values or None if ``inplace=True``. + bigframes.pandas.Series or None: + Series ordered by values or None if ``inplace=True``. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1106,6 +1538,40 @@ def sort_index( Returns a new Series sorted by label if `inplace` argument is ``False``, otherwise updates the original series and returns None. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['a', 'b', 'c', 'd'], index=[3, 2, 1, 4]) + >>> s.sort_index() + 1 c + 2 b + 3 a + 4 d + dtype: string + + Sort Descending + + >>> s.sort_index(ascending=False) + 4 d + 3 a + 2 b + 1 c + dtype: string + + By default NaNs are put at the end, but use na_position to place them at + the beginning + + >>> s = bpd.Series(['a', 'b', 'c', 'd'], index=[3, 2, 1, np.nan]) + >>> s.sort_index(na_position='first') + d + 1.0 c + 2.0 b + 3.0 a + dtype: string + Args: axis ({0 or 'index'}): Unused. Parameter needed for compatibility with DataFrame. @@ -1117,7 +1583,8 @@ def sort_index( Not implemented for MultiIndex. Returns: - bigframes.series.Series: The original Series sorted by the labels or None if + bigframes.pandas.Series or None: + The original Series sorted by the labels or None if ``inplace=True``. """ @@ -1130,21 +1597,85 @@ def nlargest( """ Return the largest `n` elements. - Args: - n (int, default 5): - Return this many descending sorted values. - keep ({'first', 'last', 'all'}, default 'first'): - When there are duplicate values that cannot all fit in a - Series of `n` elements: - ``first`` : return the first `n` occurrences in order - of appearance. - ``last`` : return the last `n` occurrences in reverse - order of appearance. - ``all`` : keep all occurrences. This can result in a Series of - size larger than `n`. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> countries_population = {"Italy": 59000000, "France": 65000000, + ... "Malta": 434000, "Maldives": 434000, + ... "Brunei": 434000, "Iceland": 337000, + ... "Nauru": 11300, "Tuvalu": 11300, + ... "Anguilla": 11300, "Montserrat": 5200} + >>> s = bpd.Series(countries_population) + >>> s + Italy 59000000 + France 65000000 + Malta 434000 + Maldives 434000 + Brunei 434000 + Iceland 337000 + Nauru 11300 + Tuvalu 11300 + Anguilla 11300 + Montserrat 5200 + dtype: Int64 + + The n largest elements where `n=5` by default. + + >>> s.nlargest() + France 65000000 + Italy 59000000 + Malta 434000 + Maldives 434000 + Brunei 434000 + dtype: Int64 + + The n largest elements where `n=3`. Default keep value is `first` so Malta + will be kept. + + >>> s.nlargest(3) + France 65000000 + Italy 59000000 + Malta 434000 + dtype: Int64 + + The n largest elements where `n=3` and keeping the last duplicates. Brunei + will be kept since it is the last with value 434000 based on the index order. + + >>> s.nlargest(3, keep='last') + France 65000000 + Italy 59000000 + Brunei 434000 + dtype: Int64 + + The n largest elements where n`=3` with all duplicates kept. Note that the + returned Series has five elements due to the three duplicates. + + >>> s.nlargest(3, keep='all') + France 65000000 + Italy 59000000 + Malta 434000 + Maldives 434000 + Brunei 434000 + dtype: Int64 + + Args: + n (int, default 5): + Return this many descending sorted values. + keep ({'first', 'last', 'all'}, default 'first'): + When there are duplicate values that cannot all fit in a + Series of `n` elements: + ``first`` : return the first `n` occurrences in order + of appearance. + ``last`` : return the last `n` occurrences in reverse + order of appearance. + ``all`` : keep all occurrences. This can result in a Series of + size larger than `n`. Returns: - bigframes.series.Series: The `n` largest values in the Series, sorted in decreasing order. + bigframes.pandas.Series: + The `n` largest values in the Series, sorted in decreasing order. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1152,6 +1683,59 @@ def nsmallest(self, n: int = 5, keep: str = "first") -> Series: """ Return the smallest `n` elements. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> countries_population = {"Italy": 59000000, "France": 65000000, + ... "Malta": 434000, "Maldives": 434000, + ... "Brunei": 434000, "Iceland": 337000, + ... "Nauru": 11300, "Tuvalu": 11300, + ... "Anguilla": 11300, "Montserrat": 5200} + >>> s = bpd.Series(countries_population) + >>> s + Italy 59000000 + France 65000000 + Malta 434000 + Maldives 434000 + Brunei 434000 + Iceland 337000 + Nauru 11300 + Tuvalu 11300 + Anguilla 11300 + Montserrat 5200 + dtype: Int64 + + The n smallest elements where `n=5` by default. + + >>> s.nsmallest() + Montserrat 5200 + Nauru 11300 + Tuvalu 11300 + Anguilla 11300 + Iceland 337000 + dtype: Int64 + + The n smallest elements where `n=3`. Default keep value is `first` so + Nauru and Tuvalu will be kept. + + >>> s.nsmallest(3) + Montserrat 5200 + Nauru 11300 + Tuvalu 11300 + dtype: Int64 + + The n smallest elements where `n=3` with all duplicates kept. Note that + the returned Series has four elements due to the three duplicates. + + >>> s.nsmallest(3, keep='all') + Montserrat 5200 + Nauru 11300 + Tuvalu 11300 + Anguilla 11300 + dtype: Int64 + Args: n (int, default 5): Return this many ascending sorted values. @@ -1167,7 +1751,8 @@ def nsmallest(self, n: int = 5, keep: str = "first") -> Series: size larger than `n`. Returns: - bigframes.series.Series: The `n` smallest values in the Series, sorted in increasing order. + bigframes.pandas.Series: + The `n` smallest values in the Series, sorted in increasing order. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1241,6 +1826,28 @@ def apply( >>> names = bpd.Series(["Alice", "Bob"]) >>> hashes = names.apply(get_hash) + You could return an array output from the remote function. + + >>> @bpd.remote_function(reuse=False) + ... def text_analyzer(text: str) -> list[int]: + ... words = text.count(" ") + 1 + ... periods = text.count(".") + ... exclamations = text.count("!") + ... questions = text.count("?") + ... return [words, periods, exclamations, questions] + + >>> texts = bpd.Series([ + ... "The quick brown fox jumps over the lazy dog.", + ... "I love this product! It's amazing.", + ... "Hungry? Wanna eat? Lets go!" + ... ]) + >>> features = texts.apply(text_analyzer) + >>> features + 0 [9 1 0 0] + 1 [6 1 1 0] + 2 [5 0 1 2] + dtype: list[pyarrow] + Simple vectorized functions, lambdas or ufuncs can be applied directly with `by_row=False`. @@ -1285,9 +1892,10 @@ def apply( the func will be passed the whole Series at once. Returns: - bigframes.series.Series: A new Series with values representing the - return value of the ``func`` applied to each element of the original - Series. + bigframes.pandas.Series: + A new Series with values representing the + return value of the ``func`` applied to each element of the + original Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1310,8 +1918,8 @@ def combine( >>> import numpy as np >>> bpd.options.display.progress_bar = None - Consider 2 Datasets ``s1`` and ``s2`` containing - highest clocked speeds of different birds. + Consider 2 Datasets ``s1`` and ``s2`` containing + highest clocked speeds of different birds. >>> s1 = bpd.Series({'falcon': 330.0, 'eagle': 160.0}) >>> s1 @@ -1325,8 +1933,8 @@ def combine( duck 30.0 dtype: Float64 - Now, to combine the two datasets and view the highest speeds - of the birds across the two datasets + Now, to combine the two datasets and view the highest speeds + of the birds across the two datasets >>> s1.combine(s2, np.maximum) falcon 345.0 @@ -1343,7 +1951,8 @@ def combine( Also accepts some numpy binary functions. Returns: - Series: The result of combining the Series with the other object. + bigframes.pandas.Series: + The result of combining the Series with the other object. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1485,7 +2094,8 @@ def groupby( If False, NA values will also be treated as the key in groups. Returns: - bigframes.core.groupby.SeriesGroupBy: Returns a groupby object that contains + bigframes.core.groupby.SeriesGroupBy: + Returns a groupby object that contains information about the groups. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1598,11 +2208,13 @@ def drop( For MultiIndex, level for which the labels will be removed. Returns: - bigframes.series.Series: Series with specified index labels removed + bigframes.pandas.Series or None: + Series with specified index labels removed or None if ``inplace=True``. Raises: - KeyError: If none of the labels are found in the index. + KeyError: + If none of the labels are found in the index. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1636,7 +2248,8 @@ def swaplevel(self, i, j): Levels of the indices to be swapped. Can pass level name as string. Returns: - Series: Series with levels swapped in MultiIndex + bigframes.pandas.Series: + Series with levels swapped in MultiIndex """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1654,7 +2267,8 @@ def droplevel(self, level, axis): For `Series` this parameter is unused and defaults to 0. Returns: - Series with requested index / column level(s) removed. + bigframes.pandas.Series: + Series with requested index / column level(s) removed. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1665,33 +2279,25 @@ def interpolate(self, method: str = "linear"): **Examples:** >>> import bigframes.pandas as bpd + >>> import numpy as np >>> bpd.options.display.progress_bar = None - >>> df = bpd.DataFrame({ - ... 'A': [1, 2, 3, None, None, 6], - ... 'B': [None, 6, None, 2, None, 3], - ... }, index=[0, 0.1, 0.3, 0.7, 0.9, 1.0]) - >>> df.interpolate() - A B - 0.0 1.0 - 0.1 2.0 6.0 - 0.3 3.0 4.0 - 0.7 4.0 2.0 - 0.9 5.0 2.5 - 1.0 6.0 3.0 - - [6 rows x 2 columns] - >>> df.interpolate(method="values") - A B - 0.0 1.0 - 0.1 2.0 6.0 - 0.3 3.0 4.666667 - 0.7 4.714286 2.0 - 0.9 5.571429 2.666667 - 1.0 6.0 3.0 - - [6 rows x 2 columns] + Filling in NaN in a Series via linear interpolation. + + >>> s = bpd.Series([0, 1, np.nan, 3]) + >>> s + 0 0.0 + 1 1.0 + 2 + 3 3.0 + dtype: Float64 + >>> s.interpolate() + 0 0.0 + 1 1.0 + 2 2.0 + 3 3.0 + dtype: Float64 Args: method (str, default 'linear'): @@ -1702,7 +2308,7 @@ def interpolate(self, method: str = "linear"): 'pad': Fill in NaNs using existing values. 'nearest', 'zero', 'slinear': Emulates `scipy.interpolate.interp1d` Returns: - Series: + bigframes.pandas.Series: Returns the same object type as the caller, interpolated at some or all ``NaN`` values """ @@ -1718,6 +2324,7 @@ def fillna( **Examples:** >>> import bigframes.pandas as bpd + >>> import numpy as np >>> bpd.options.display.progress_bar = None >>> s = bpd.Series([np.nan, 2, np.nan, -1]) @@ -1752,7 +2359,8 @@ def fillna( Value to use to fill holes (e.g. 0). Returns: - Series or None: Object with missing values filled or None. + bigframes.pandas.Series or None: + Object with missing values filled or None. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1874,7 +2482,8 @@ def replace( string. Returns: - Series/DataFrame: Object after replacement. + bigframes.pandas.Series or bigframes.pandas.DataFrame: + Object after replacement. Raises: TypeError: @@ -1897,6 +2506,7 @@ def dropna(self, *, axis=0, inplace: bool = False, how=None) -> Series: **Examples:** >>> import bigframes.pandas as bpd + >>> import numpy as np >>> bpd.options.display.progress_bar = None Drop NA values from a Series: @@ -1939,7 +2549,8 @@ def dropna(self, *, axis=0, inplace: bool = False, how=None) -> Series: Not in use. Kept for compatibility. Returns: - Series: Series with NA entries dropped from it. + bigframes.pandas.Series: + Series with NA entries dropped from it. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1959,6 +2570,7 @@ def between( **Examples:** >>> import bigframes.pandas as bpd + >>> import numpy as np >>> bpd.options.display.progress_bar = None Boundary values are included by default: @@ -2001,8 +2613,9 @@ def between( Include boundaries. Whether to set each bound as closed or open. Returns: - Series: Series representing whether each element is between left and - right (inclusive). + bigframes.pandas.Series: + Series representing whether each element is between left and + right (inclusive). """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2016,6 +2629,7 @@ def case_when( **Examples:** >>> import bigframes.pandas as bpd + >>> import numpy as np >>> bpd.options.display.progress_bar = None >>> c = bpd.Series([6, 7, 8, 9], name="c") @@ -2036,11 +2650,10 @@ def case_when( **See also:** - - :func:`bigframes.series.Series.mask` : Replace values where the condition is True. + - :func:`bigframes.pandas.Series.mask` : Replace values where the condition is True. Args: - caselist: - A list of tuples of conditions and expected replacements + caselist (A list of tuples of conditions and expected replacements): Takes the form: ``(condition0, replacement0)``, ``(condition1, replacement1)``, ... . ``condition`` should be a 1-D boolean array-like object @@ -2056,7 +2669,7 @@ def case_when( (though pandas doesn`t check it). Returns: - bigframes.series.Series + bigframes.pandas.Series """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2070,6 +2683,7 @@ def cumprod(self): **Examples:** >>> import bigframes.pandas as bpd + >>> import numpy as np >>> bpd.options.display.progress_bar = None >>> s = bpd.Series([2, np.nan, 5, -1, 0]) @@ -2092,7 +2706,8 @@ def cumprod(self): dtype: Float64 Returns: - bigframes.series.Series: Return cumulative sum of scalar or Series. + bigframes.pandas.Series: + Return cumulative sum of scalar or Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2103,13 +2718,39 @@ def cumsum(self): Returns a DataFrame or Series of the same size containing the cumulative sum. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([2, np.nan, 5, -1, 0]) + >>> s + 0 2.0 + 1 + 2 5.0 + 3 -1.0 + 4 0.0 + dtype: Float64 + + By default, NA values are ignored. + + >>> s.cumsum() + 0 2.0 + 1 + 2 7.0 + 3 6.0 + 4 6.0 + dtype: Float64 + Args: axis ({0 or 'index', 1 or 'columns'}, default 0): The index or the name of the axis. 0 is equivalent to None or 'index'. For `Series` this parameter is unused and defaults to 0. Returns: - scalar or Series: Return cumulative sum of scalar or Series. + bigframes.pandas.Series: + Return cumulative sum of scalar or Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2120,13 +2761,35 @@ def cummax(self): Returns a DataFrame or Series of the same size containing the cumulative maximum. - Args: - axis ({{0 or 'index', 1 or 'columns'}}, default 0): - The index or the name of the axis. 0 is equivalent to None or 'index'. - For `Series` this parameter is unused and defaults to 0. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([2, np.nan, 5, -1, 0]) + >>> s + 0 2.0 + 1 + 2 5.0 + 3 -1.0 + 4 0.0 + dtype: Float64 + + By default, NA values are ignored. + + >>> s.cummax() + 0 2.0 + 1 + 2 5.0 + 3 5.0 + 4 5.0 + dtype: Float64 + Returns: - bigframes.series.Series: Return cumulative maximum of scalar or Series. + bigframes.pandas.Series: + Return cumulative maximum of scalar or Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2137,30 +2800,75 @@ def cummin(self): Returns a DataFrame or Series of the same size containing the cumulative minimum. - Args: - axis ({0 or 'index', 1 or 'columns'}, default 0): - The index or the name of the axis. 0 is equivalent to None or 'index'. - For `Series` this parameter is unused and defaults to 0. - skipna (bool, default True): - Exclude NA/null values. If an entire row/column is NA, the result - will be NA. - `*args`, `**kwargs`: - Additional keywords have no effect but might be accepted for - compatibility with NumPy. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([2, np.nan, 5, -1, 0]) + >>> s + 0 2.0 + 1 + 2 5.0 + 3 -1.0 + 4 0.0 + dtype: Float64 + + By default, NA values are ignored. + + >>> s.cummin() + 0 2.0 + 1 + 2 2.0 + 3 -1.0 + 4 -1.0 + dtype: Float64 Returns: - bigframes.series.Series: Return cumulative minimum of scalar or Series. + bigframes.pandas.Series: + Return cumulative minimum of scalar or Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def eq(self, other) -> Series: """Return equal of Series and other, element-wise (binary operator eq). - Equivalent to ``other == series``, but with support to substitute a fill_value for - missing data in either one of the inputs. + Equivalent to ``other == series``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 + + >>> a.eq(b) + a True + b + c + d + e + dtype: boolean Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: Series: The result of the operation. @@ -2171,83 +2879,241 @@ def eq(self, other) -> Series: def ne(self, other) -> Series: """Return not equal of Series and other, element-wise (binary operator ne). - Equivalent to ``other != series``, but with support to substitute a fill_value for - missing data in either one of the inputs. + Equivalent to ``other != series``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 + + >>> a.ne(b) + a False + b + c + d + e + dtype: boolean Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def le(self, other) -> Series: - """Get 'less than or equal to' of Series and other, element-wise (binary operator `<=`). + """Get 'less than or equal to' of Series and other, element-wise (binary + operator le). + + Equivalent to ``series <= other``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 - Equivalent to ``series <= other``, but with support to substitute a fill_value for - missing data in either one of the inputs. + >>> a.le(b) + a True + b + c + d + e + dtype: boolean Args: other: Series, or scalar value Returns: - bigframes.series.Series: The result of the comparison. + bigframes.pandas.Series: + The result of the comparison. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def lt(self, other) -> Series: - """Get 'less than' of Series and other, element-wise (binary operator `<`). + """Get 'less than' of Series and other, element-wise (binary operator lt). + + Equivalent to ``series < other``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 - Equivalent to ``series < other``, but with support to substitute a fill_value for - missing data in either one of the inputs. + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 + + >>> a.lt(b) + a False + b + c + d + e + dtype: boolean Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def ge(self, other) -> Series: - """Get 'greater than or equal to' of Series and other, element-wise (binary operator `>=`). + """Get 'greater than or equal to' of Series and other, element-wise + (binary operator ge). + + Equivalent to ``series >= other``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 - Equivalent to ``series >= other``, but with support to substitute a fill_value for - missing data in either one of the inputs. + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 + + >>> a.ge(b) + a True + b + c + d + e + dtype: boolean Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def gt(self, other) -> Series: - """Get 'less than or equal to' of Series and other, element-wise (binary operator `<=`). + """Return Greater than of series and other, element-wise + (binary operator gt). + + Equivalent to ``series <= other``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 - Equivalent to ``series <= other``, but with support to substitute a fill_value for - missing data in either one of the inputs. + >>> a.gt(b) + a False + b + c + d + e + dtype: boolean Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def add(self, other) -> Series: - """Return addition of Series and other, element-wise (binary operator add). + """Return addition of Series and other, element-wise (binary operator + add). - Equivalent to ``series + other``, but with support to substitute a fill_value for - missing data in either one of the inputs. + Equivalent to ``series + other``, but with support to substitute a + fill_value for missing data in either one of the inputs. **Examples:** @@ -2299,10 +3165,11 @@ def add(self, other) -> Series: dtype: Int64 Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2352,21 +3219,54 @@ def __add__(self, other): Object to be added to the Series. Returns: - Series: The result of adding `other` to Series. + bigframes.pandas.Series: + The result of adding `other` to Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def radd(self, other) -> Series: - """Return addition of Series and other, element-wise (binary operator radd). + """Return addition of Series and other, element-wise (binary operator + radd). + + Equivalent to ``other + series``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 - Equivalent to ``other + series``, but with support to substitute a fill_value for - missing data in either one of the inputs. + >>> a.add(b) + a 2.0 + b + c + d + e + dtype: Float64 Args: - other (Series, or scalar value): + other (Series, or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2381,7 +3281,8 @@ def __radd__(self, other): Object to which Series should be added. Returns: - Series: The result of adding Series to `other`. + bigframes.pandas.Series: + The result of adding Series to `other`. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2389,16 +3290,48 @@ def sub( self, other, ) -> Series: - """Return subtraction of Series and other, element-wise (binary operator sub). + """Return subtraction of Series and other, element-wise (binary operator + sub). - Equivalent to ``series - other``, but with support to substitute a fill_value for - missing data in either one of the inputs. + Equivalent to ``series - other``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 + + >>> a.subtract(b) + a 0.0 + b + c + d + e + dtype: Float64 Args: - other (Series, or scalar value): + other (Series, or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2448,21 +3381,54 @@ def __sub__(self, other): Object to subtract from the Series. Returns: - Series: The result of subtraction. + bigframes.pandas.Series: + The result of subtraction. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rsub(self, other) -> Series: - """Return subtraction of Series and other, element-wise (binary operator rsub). + """Return subtraction of Series and other, element-wise (binary operator + rsub). + + Equivalent to ``other - series``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 - Equivalent to ``other - series``, but with support to substitute a fill_value for - missing data in either one of the inputs. + >>> a.subtract(b) + a 0.0 + b + c + d + e + dtype: Float64 Args: - other (Series, or scalar value): + other (Series, or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2477,21 +3443,54 @@ def __rsub__(self, other): Object to subtract the Series from. Returns: - Series: The result of subtraction. + bigframes.pandas.Series: + The result of subtraction. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def mul(self, other) -> Series: - """Return multiplication of Series and other, element-wise (binary operator mul). + """Return multiplication of Series and other, element-wise (binary + operator mul). + + Equivalent to ``other * series``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 - Equivalent to ``other * series``, but with support to substitute a fill_value for - missing data in either one of the inputs. + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 + + >>> a.multiply(b) + a 1.0 + b + c + d + e + dtype: Float64 Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2530,21 +3529,54 @@ def __mul__(self, other): Object to multiply with the Series. Returns: - Series: The result of the multiplication. + bigframes.pandas.Series: + The result of the multiplication. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rmul(self, other) -> Series: - """Return multiplication of Series and other, element-wise (binary operator mul). + """Return multiplication of Series and other, element-wise (binary + operator mul). + + Equivalent to ``series * others``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 - Equivalent to ``series * others``, but with support to substitute a fill_value for - missing data in either one of the inputs. + >>> a.multiply(b) + a 1.0 + b + c + d + e + dtype: Float64 Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: - Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2559,21 +3591,53 @@ def __rmul__(self, other): Object to multiply the Series with. Returns: - Series: The result of the multiplication. + bigframes.pandas.Series: The result of the multiplication. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def truediv(self, other) -> Series: - """Return floating division of Series and other, element-wise (binary operator truediv). + """Return floating division of Series and other, element-wise (binary + operator truediv). + + Equivalent to ``series / other``, but with support to substitute a + fill_value for missing data in either one of the inputs. - Equivalent to ``series / other``, but with support to substitute a fill_value for - missing data in either one of the inputs. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 + + >>> a.divide(b) + a 1.0 + b + c + d + e + dtype: Float64 Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2612,21 +3676,54 @@ def __truediv__(self, other): Object to divide the Series by. Returns: - Series: The result of the division. + bigframes.pandas.Series: + The result of the division. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rtruediv(self, other) -> Series: - """Return floating division of Series and other, element-wise (binary operator rtruediv). + """Return floating division of Series and other, element-wise (binary + operator rtruediv). + + Equivalent to ``other / series``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 - Equivalent to ``other / series``, but with support to substitute a fill_value for - missing data in either one of the inputs. + >>> a.divide(b) + a 1.0 + b + c + d + e + dtype: Float64 Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2642,21 +3739,53 @@ def __rtruediv__(self, other): Object to divide by the Series. Returns: - Series: The result of the division. + bigframes.pandas.Series: The result of the division. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def floordiv(self, other) -> Series: - """Return integer division of Series and other, element-wise (binary operator floordiv). + """Return integer division of Series and other, element-wise + (binary operator floordiv). + + Equivalent to ``series // other``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 - Equivalent to ``series // other``, but with support to substitute a fill_value for - missing data in either one of the inputs. + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 + + >>> a.floordiv(b) + a 1.0 + b + c + d + e + dtype: Float64 Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2695,21 +3824,54 @@ def __floordiv__(self, other): Object to divide the Series by. Returns: - Series: The result of the integer divison. + bigframes.pandas.Series: + The result of the integer division. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rfloordiv(self, other) -> Series: - """Return integer division of Series and other, element-wise (binary operator rfloordiv). + """Return integer division of Series and other, element-wise (binary + operator rfloordiv). + + Equivalent to ``other // series``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 - Equivalent to ``other // series``, but with support to substitute a fill_value for - missing data in either one of the inputs. + >>> a.floordiv(b) + a 1.0 + b + c + d + e + dtype: Float64 Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2725,21 +3887,53 @@ def __rfloordiv__(self, other): Object to divide by the Series. Returns: - Series: The result of the integer division. + bigframes.pandas.Series: + The result of the integer division. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def mod(self, other) -> Series: """Return modulo of Series and other, element-wise (binary operator mod). - Equivalent to ``series % other``, but with support to substitute a fill_value for - missing data in either one of the inputs. + Equivalent to ``series % other``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 + + >>> a.mod(b) + a 0.0 + b + c + d + e + dtype: Float64 Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2778,21 +3972,53 @@ def __mod__(self, other): Object to modulo the Series by. Returns: - Series: The result of the modulo. + bigframes.pandas.Series: + The result of the modulo. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rmod(self, other) -> Series: """Return modulo of Series and other, element-wise (binary operator mod). - Equivalent to ``series % other``, but with support to substitute a fill_value for - missing data in either one of the inputs. + Equivalent to ``series % other``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 + + >>> a.mod(b) + a 0.0 + b + c + d + e + dtype: Float64 Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2808,21 +4034,54 @@ def __rmod__(self, other): Object to modulo by the Series. Returns: - Series: The result of the modulo. + bigframes.pandas.Series: + The result of the modulo. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def pow(self, other) -> Series: - """Return Exponential power of series and other, element-wise (binary operator `pow`). + """Return Exponential power of series and other, element-wise (binary + operator `pow`). + + Equivalent to ``series ** other``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 - Equivalent to ``series ** other``, but with support to substitute a fill_value for - missing data in either one of the inputs. + >>> a.pow(b) + a 1.0 + b 1.0 + c 1.0 + d + e + dtype: Float64 Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2861,22 +4120,55 @@ def __pow__(self, other): other (scalar or Series): Object to exponentiate the Series with. - Returns: - Series: The result of the exponentiation. - """ - raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + Returns: + bigframes.pandas.Series: + The result of the exponentiation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def rpow(self, other) -> Series: + """Return Exponential power of series and other, element-wise (binary + operator `rpow`). + + Equivalent to ``other ** series``, but with support to substitute a + fill_value for missing data in either one of the inputs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None - def rpow(self, other) -> Series: - """Return Exponential power of series and other, element-wise (binary operator `rpow`). + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 - Equivalent to ``other ** series``, but with support to substitute a fill_value for - missing data in either one of the inputs. + >>> a.pow(b) + a 1.0 + b 1.0 + c 1.0 + d + e + dtype: Float64 Args: - other (Series, or scalar value): + other (Series or scalar value) Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2893,21 +4185,60 @@ def __rpow__(self, other): Object to exponentiate with the Series. Returns: - Series: The result of the exponentiation. + bigframes.pandas.Series: + The result of the exponentiation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def divmod(self, other) -> Series: - """Return integer division and modulo of Series and other, element-wise (binary operator divmod). + """Return integer division and modulo of Series and other, element-wise + (binary operator divmod). Equivalent to divmod(series, other). + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 + + >>> a.divmod(b) + (a 1.0 + b + c + d + e + dtype: Float64, + a 0.0 + b + c + d + e + dtype: Float64) + Args: other: Series, or scalar value Returns: - 2-Tuple of Series: The result of the operation. The result is always - consistent with (floordiv, mod) (though pandas may not). + Tuple[bigframes.pandas.Series, bigframes.pandas.Series]: + The result of the operation. The result is always + consistent with (floordiv, mod) (though pandas may not). """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2917,12 +4248,49 @@ def rdivmod(self, other) -> Series: Equivalent to other divmod series. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 1, 1, np.nan], index=['a', 'b', 'c', 'd']) + >>> a + a 1.0 + b 1.0 + c 1.0 + d + dtype: Float64 + + >>> b = bpd.Series([1, np.nan, 1, np.nan], index=['a', 'b', 'd', 'e']) + >>> b + a 1.0 + b + d 1.0 + e + dtype: Float64 + + >>> a.divmod(b) + (a 1.0 + b + c + d + e + dtype: Float64, + a 0.0 + b + c + d + e + dtype: Float64) + Args: other: Series, or scalar value Returns: - 2-Tuple of Series: The result of the operation. The result is always - consistent with (rfloordiv, rmod) (though pandas may not). + Tuple[bigframes.pandas.Series, bigframes.pandas.Series]: + The result of the operation. The result is always + consistent with (rfloordiv, rmod) (though pandas may not). """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2949,8 +4317,8 @@ def combine_first(self, other) -> Series: 2 5.0 dtype: Float64 - Null values still persist if the location of that null value - does not exist in `other` + Null values still persist if the location of that null value + does not exist in `other` >>> s1 = bpd.Series({'falcon': np.nan, 'eagle': 160.0}) >>> s2 = bpd.Series({'eagle': 200.0, 'duck': 30.0}) @@ -2965,7 +4333,8 @@ def combine_first(self, other) -> Series: The value(s) to be used for filling null values. Returns: - Series: The result of combining the provided Series with the other object. + bigframes.pandas.Series: + The result of combining the provided Series with the other object. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3018,8 +4387,8 @@ def update(self, other) -> None: 2 6 dtype: Int64 - ``other`` can also be a non-Series object type - that is coercible into a Series + ``other`` can also be a non-Series object type + that is coercible into a Series >>> s = bpd.Series([1, 2, 3]) >>> s.update([4, np.nan, 6]) @@ -3039,6 +4408,9 @@ def update(self, other) -> None: Args: other (Series, or object coercible into Series) + + Returns: + None """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3052,7 +4424,8 @@ def all( DataFrame axis that is False or equivalent (e.g. zero or empty). Returns: - scalar or Series: If level is specified, then, Series is returned; + scalar or bigframes.pandas.Series: + If level is specified, then, Series is returned; otherwise, scalar is returned. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3066,8 +4439,29 @@ def any( Returns False unless there is at least one element within a series or along a Dataframe axis that is True or equivalent (e.g. non-zero or non-empty). + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + For Series input, the output is a scalar indicating whether any element is True. + + >>> bpd.Series([False, False]).any() + np.False_ + + >>> bpd.Series([True, False]).any() + np.True_ + + >>> bpd.Series([], dtype="float64").any() + np.False_ + + >>> bpd.Series([np.nan]).any() + np.False_ + Returns: - scalar or Series: If level is specified, then, Series is returned; + scalar or bigframes.pandas.Series: + If level is specified, then, Series is returned; otherwise, scalar is returned. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3093,6 +4487,7 @@ def max( 0 1 1 3 dtype: Int64 + >>> s.max() np.int64(3) @@ -3104,6 +4499,7 @@ def max( 1 3 2 dtype: Int64 + >>> s.max() np.int64(3) @@ -3133,6 +4529,7 @@ def min( 0 1 1 3 dtype: Int64 + >>> s.min() np.int64(1) @@ -3144,6 +4541,7 @@ def min( 1 3 2 dtype: Int64 + >>> s.min() np.int64(1) @@ -3184,9 +4582,8 @@ def std( height 0.237417 dtype: Float64 - Returns - ------- - scalar or Series (if level specified) + Returns: + scalar or bigframes.pandas.Series (if level specified) """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3199,7 +4596,8 @@ def var( Normalized by N-1 by default. Returns: - scalar or Series (if level specified): Variance. + scalar or bigframes.pandas.Series (if level specified): + Variance. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3220,6 +4618,7 @@ def sum(self): 0 1 1 3 dtype: Int64 + >>> s.sum() np.int64(4) @@ -3231,6 +4630,7 @@ def sum(self): 1 3 2 dtype: Int64 + >>> s.sum() np.int64(4) @@ -3254,6 +4654,7 @@ def mean(self): 0 1 1 3 dtype: Int64 + >>> s.mean() np.float64(2.0) @@ -3265,6 +4666,7 @@ def mean(self): 1 3 2 dtype: Int64 + >>> s.mean() np.float64(2.0) @@ -3276,6 +4678,30 @@ def mean(self): def median(self, *, exact: bool = True): """Return the median of the values over the requested axis. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 3]) + >>> s.median() + np.float64(2.0) + + With a DataFrame + + >>> df = bpd.DataFrame({'a': [1, 2], 'b': [2, 3]}, index=['tiger', 'zebra']) + >>> df + a b + tiger 1 2 + zebra 2 3 + + [2 rows x 2 columns] + + >>> df.median() + a 1.5 + b 2.5 + dtype: Float64 + Args: exact (bool. default True): Default True. Get the exact median instead of an approximate @@ -3297,9 +4723,11 @@ def quantile( >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series([1, 2, 3, 4]) >>> s.quantile(.5) np.float64(2.5) + >>> s.quantile([.25, .5, .75]) 0.25 1.75 0.5 2.5 @@ -3307,11 +4735,11 @@ def quantile( dtype: Float64 Args: - q (float or array-like, default 0.5 (50% quantile)): + q (Union[float, Sequence[float], default 0.5 (50% quantile)): The quantile(s) to compute, which can lie in range: 0 <= q <= 1. Returns: - float or Series: + Union[float, bigframes.pandas.Series]: If ``q`` is an array, a Series will be returned where the index is ``q`` and the values are the quantiles, otherwise a float will be returned. @@ -3331,6 +4759,33 @@ def skew(self): Normalized by N-1. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 3]) + >>> s.skew() + np.float64(0.0) + + With a DataFrame + + >>> df = bpd.DataFrame({'a': [1, 2, 3], 'b': [2, 3, 4], 'c': [1, 3, 5]}, + ... index=['tiger', 'zebra', 'cow']) + >>> df + a b c + tiger 1 2 1 + zebra 2 3 3 + cow 3 4 5 + + [3 rows x 3 columns] + + >>> df.skew() + a 0.0 + b 0.0 + c 0.0 + dtype: Float64 + Returns: scalar: Scalar. """ @@ -3339,46 +4794,72 @@ def skew(self): def kurt(self): """Return unbiased kurtosis over requested axis. - Kurtosis obtained using Fisher’s definition of kurtosis (kurtosis of normal == 0.0). Normalized by N-1. + Kurtosis obtained using Fisher’s definition of kurtosis (kurtosis of + normal == 0.0). Normalized by N-1. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 2, 3], index=['cat', 'dog', 'dog', 'mouse']) + >>> s + cat 1 + dog 2 + dog 2 + mouse 3 + dtype: Int64 + + >>> s.kurt() + np.float64(1.5) + + With a DataFrame + + >>> df = bpd.DataFrame({'a': [1, 2, 2, 3], 'b': [3, 4, 4, 4]}, + ... index=['cat', 'dog', 'dog', 'mouse']) + >>> df + a b + cat 1 3 + dog 2 4 + dog 2 4 + mouse 3 4 + + [4 rows x 2 columns] + + >>> df.kurt() + a 1.5 + b 4.0 + dtype: Float64 Returns: - scalar or scalar: Unbiased kurtosis over requested axis. + scalar or scalar: + Unbiased kurtosis over requested axis. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def items(self): """ - Iterate over (index, value) pairs of a Series. + Lazily iterate over (index, value) tuples. - Iterates over the Series contents, returning a tuple with - the index and the value of a Series. + This method returns an iterable tuple (index, value). + This is convenient if you want to create a lazy iterator. **Examples:** >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None - >>> s = bpd.Series(['bear', 'bear', 'marsupial'], - ... index=['panda', 'polar', 'koala']) - >>> s - panda bear - polar bear - koala marsupial - dtype: string - + >>> s = bpd.Series(['A', 'B', 'C']) >>> for index, value in s.items(): - ... print(f'--> index: {index}') - ... print(f'--> value: {value}') - ... - --> index: panda - --> value: bear - --> index: polar - --> value: bear - --> index: koala - --> value: marsupial + ... print(f"Index : {index}, Value : {value}") + Index : 0, Value : A + Index : 1, Value : B + Index : 2, Value : C Returns: - Iterator: Iterator of index, value for each content of the Series. + iterable: + Iterable of tuples containing the (index, value) pairs from a + Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3445,7 +4926,8 @@ def where(self, cond, other): extension dtypes). Returns: - bigframes.series.Series: Series after the replacement. + bigframes.pandas.Series: + Series after the replacement. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3567,7 +5049,8 @@ def mask(self, cond, other): extension dtypes). Returns: - bigframes.series.Series: Series after the replacement. + bigframes.pandas.Series: + Series after the replacement. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3580,13 +5063,16 @@ def clip(self, lower, upper): Args: lower (float or array-like, default None): - Minimum threshold value. All values below this threshold will be set to it. A missing threshold (e.g NA) will not clip the value. + Minimum threshold value. All values below this threshold will + be set to it. A missing threshold (e.g NA) will not clip the value. upper (float or array-like, default None): - Maximum threshold value. All values above this threshold will be set to it. A missing threshold (e.g NA) will not clip the value. + Maximum threshold value. All values above this threshold will + be set to it. A missing threshold (e.g NA) will not clip the value. Returns: - Series: Series. + bigframes.pandas.Series: + Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3594,20 +5080,17 @@ def unstack(self, level): """ Unstack, also known as pivot, Series with MultiIndex to produce DataFrame. - Args: - level (int, str, or list of these, default last level): - Level(s) to unstack, can pass level name. - Returns: - DataFrame: Unstacked Series. + bigframes.pandas.DataFrame: Unstacked Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def argmax(self): """ - Return int position of the smallest value in the series. + Return int position of the largest value in the series. - If the minimum is achieved in multiple locations, the first row position is returned. + If the maximum is achieved in multiple locations, the first row position + is returned. **Examples:** @@ -3635,15 +5118,17 @@ def argmax(self): calories is the first element, since series is zero-indexed. Returns: - Series: Row position of the maximum value. + int: + Row position of the maximum value. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def argmin(self): """ - Return int position of the largest value in the Series. + Return int position of the smallest value in the Series. - If the maximum is achieved in multiple locations, the first row position is returned. + If the minimum is achieved in multiple locations, the first row position + is returned. **Examples:** @@ -3671,7 +5156,8 @@ def argmin(self): calories is the first element, since series is zero-indexed. Returns: - Series: Row position of the minimum value. + int: + Row position of the minimum value. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3721,7 +5207,8 @@ def rename(self, index, **kwargs) -> Series | None: attribute. Returns: - bigframes.series.Series: Series with index labels. + bigframes.pandas.Series: + Series with index labels. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3734,8 +5221,53 @@ def rename_axis(self, mapper, **kwargs): mapper (scalar, list-like, optional): Value to set the axis name attribute. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Series + + >>> s = bpd.Series(["dog", "cat", "monkey"]) + >>> s + 0 dog + 1 cat + 2 monkey + dtype: string + + >>> s.rename_axis("animal") + animal + 0 dog + 1 cat + 2 monkey + dtype: string + + DataFrame + + >>> df = bpd.DataFrame({"num_legs": [4, 4, 2], + ... "num_arms": [0, 0, 2]}, + ... ["dog", "cat", "monkey"]) + >>> df + num_legs num_arms + dog 4 0 + cat 4 0 + monkey 2 2 + + [3 rows x 2 columns] + + >>> df = df.rename_axis("animal") + >>> df + num_legs num_arms + animal + dog 4 0 + cat 4 0 + monkey 2 2 + + [3 rows x 2 columns] + Returns: - bigframes.series.Series: Series with the name of the axis set. + bigframes.pandas.Series or bigframes.pandas.DataFrame: + The same type as the caller. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3819,7 +5351,8 @@ def value_counts( Don't include counts of NaN. Returns: - Series: Series containing counts of unique values. + bigframes.pandas.Series: + Series containing counts of unique values. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3860,6 +5393,16 @@ def plot(self): """ Make plots of Series. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> ser = bpd.Series([1, 2, 3, 3]) + >>> plot = ser.plot(kind='hist', title="My plot") + >>> plot + + Returns: bigframes.operations.plotting.PlotAccessor: An accessor making plots. @@ -3894,9 +5437,23 @@ def isin(self, values): 5 hippo Name: animal, dtype: string - >>> s.isin(['cow', 'llama']) + To invert the boolean values, use the ~ operator: + + >>> ~s.isin(['cow', 'llama']) + 0 False + 1 False + 2 False + 3 True + 4 False + 5 True + Name: animal, dtype: boolean + + Passing a single string as s.isin('llama') will raise an error. Use a + list of one element instead: + + >>> s.isin(['llama']) 0 True - 1 True + 1 False 2 True 3 False 4 True @@ -3918,7 +5475,7 @@ def isin(self, values): TypeError. Instead, turn a single string into a list of one element. Returns: - bigframes.series.Series: Series of booleans indicating if each element is in values. + bigframes.pandas.Series: Series of booleans indicating if each element is in values. Raises: TypeError: If input is not list-like. @@ -3944,7 +5501,8 @@ def is_monotonic_increasing(self) -> bool: np.False_ Returns: - bool: Boolean. + bool: + Boolean. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3967,7 +5525,8 @@ def is_monotonic_decreasing(self) -> bool: np.False_ Returns: - bool: Boolean. + bool: + Boolean. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -4051,7 +5610,8 @@ def map( index entry. Returns: - Series: Same index as caller. + bigframes.pandas.Series: + Same index as caller. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -4059,8 +5619,79 @@ def map( def iloc(self): """Purely integer-location based indexing for selection by position. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> mydict = [{'a': 1, 'b': 2, 'c': 3, 'd': 4}, + ... {'a': 100, 'b': 200, 'c': 300, 'd': 400}, + ... {'a': 1000, 'b': 2000, 'c': 3000, 'd': 4000}] + >>> df = bpd.DataFrame(mydict) + >>> df + a b c d + 0 1 2 3 4 + 1 100 200 300 400 + 2 1000 2000 3000 4000 + + [3 rows x 4 columns] + + Indexing just the rows + + With a scalar integer. + + >>> type(df.iloc[0]) + + + >>> df.iloc[0] + a 1 + b 2 + c 3 + d 4 + Name: 0, dtype: Int64 + + With a list of integers. + + >>> df.iloc[0] + a 1 + b 2 + c 3 + d 4 + Name: 0, dtype: Int64 + + >>> type(df.iloc[[0]]) + + + >>> df.iloc[[0, 1]] + a b c d + 0 1 2 3 4 + 1 100 200 300 400 + + [2 rows x 4 columns] + + With a slice object. + + >>> df.iloc[:3] + a b c d + 0 1 2 3 4 + 1 100 200 300 400 + 2 1000 2000 3000 4000 + + [3 rows x 4 columns] + + Indexing both axes + + You can mix the indexer types for the index and columns. Use : to select + the entire axis. + + With scalar integers. + + >>> df.iloc[0, 1] + np.int64(2) + Returns: - bigframes.core.indexers.IlocSeriesIndexer: Purely integer-location Indexers. + bigframes.core.indexers.IlocSeriesIndexer: + Purely integer-location Indexers. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -4068,8 +5699,86 @@ def iloc(self): def loc(self): """Access a group of rows and columns by label(s) or a boolean array. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame([[1, 2], [4, 5], [7, 8]], + ... index=['cobra', 'viper', 'sidewinder'], + ... columns=['max_speed', 'shield']) + >>> df + max_speed shield + cobra 1 2 + viper 4 5 + sidewinder 7 8 + + [3 rows x 2 columns] + + Single label. Note this returns the row as a Series. + + >>> df.loc['viper'] + max_speed 4 + shield 5 + Name: viper, dtype: Int64 + + List of labels. Note using [[]] returns a DataFrame. + + >>> df.loc[['viper', 'sidewinder']] + max_speed shield + viper 4 5 + sidewinder 7 8 + + [2 rows x 2 columns] + + Slice with labels for row and single label for column. As mentioned + above, note that both the start and stop of the slice are included. + + >>> df.loc['cobra', 'shield'] + np.int64(2) + + Index (same behavior as df.reindex) + + >>> df.loc[bpd.Index(["cobra", "viper"], name="foo")] + max_speed shield + cobra 1 2 + viper 4 5 + + [2 rows x 2 columns] + + Conditional that returns a boolean Series with column labels specified + + >>> df.loc[df['shield'] > 6, ['max_speed']] + max_speed + sidewinder 7 + + [1 rows x 1 columns] + + Multiple conditional using | that returns a boolean Series + + >>> df.loc[(df['max_speed'] > 4) | (df['shield'] < 5)] + max_speed shield + cobra 1 2 + sidewinder 7 8 + + [2 rows x 2 columns] + + Please ensure that each condition is wrapped in parentheses (). + + Set value for an entire column + + >>> df.loc[:, 'max_speed'] = 30 + >>> df + max_speed shield + cobra 30 2 + viper 30 5 + sidewinder 30 8 + + [3 rows x 2 columns] + Returns: - bigframes.core.indexers.LocSeriesIndexer: Indexers object. + bigframes.core.indexers.LocSeriesIndexer: + Indexers object. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -4080,21 +5789,31 @@ def iat(self): **Examples:** >>> import bigframes.pandas as bpd - >>> s = bpd.Series(bpd.Series([1, 2, 3])) >>> bpd.options.display.progress_bar = None - >>> s - 0 1 - 1 2 - 2 3 - dtype: Int64 - Get value at specified row number + >>> df = bpd.DataFrame([[0, 2, 3], [0, 4, 1], [10, 20, 30]], + ... columns=['A', 'B', 'C']) + >>> df + A B C + 0 0 2 3 + 1 0 4 1 + 2 10 20 30 + + [3 rows x 3 columns] + + Get value at specified row/column pair + + >>> df.iat[1, 2] + np.int64(1) + + Get value within a series - >>> s.iat[1] + >>> df.loc[0].iat[1] np.int64(2) Returns: - bigframes.core.indexers.IatSeriesIndexer: Indexers object. + bigframes.core.indexers.IatSeriesIndexer: + Indexers object. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -4105,22 +5824,31 @@ def at(self): **Examples:** >>> import bigframes.pandas as bpd - >>> s = bpd.Series([1, 2, 3], index=['A', 'B', 'C']) >>> bpd.options.display.progress_bar = None - >>> s - A 1 - B 2 - C 3 - dtype: Int64 - Get value at specified row label + >>> df = bpd.DataFrame([[0, 2, 3], [0, 4, 1], [10, 20, 30]], + ... index=[4, 5, 6], columns=['A', 'B', 'C']) + >>> df + A B C + 4 0 2 3 + 5 0 4 1 + 6 10 20 30 + + [3 rows x 3 columns] + + Get value at specified row/column pair - >>> s.at['B'] + >>> df.at[4, 'B'] np.int64(2) + Get value at specified row label + + >>> df.loc[5].at['B'] + np.int64(4) Returns: - bigframes.core.indexers.AtSeriesIndexer: Indexers object. + bigframes.core.indexers.AtSeriesIndexer: + Indexers object. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -4141,7 +5869,8 @@ def values(self): array(['a', 'a', 'b', 'c'], dtype=object) Returns: - numpy.ndarray or ndarray-like: Values in the Series. + numpy.ndarray or ndarray-like: + Values in the Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -4157,7 +5886,12 @@ def size(self) -> int: For Series: - >>> s = bpd.Series({'a': 1, 'b': 2, 'c': 3}) + >>> s = bpd.Series(['Ant', 'Bear', 'Cow']) + >>> s + 0 Ant + 1 Bear + 2 Cow + dtype: string >>> s.size 3 @@ -4168,7 +5902,8 @@ def size(self) -> int: 3 Returns: - int: Return the number of elements in the underlying data. + int: + Return the number of elements in the underlying data. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -4236,7 +5971,8 @@ def __invert__(self): dtype: boolean Returns: - Series: The inverted values in the series. + bigframes.pandas.Series: + The inverted values in the series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -4274,7 +6010,8 @@ def __and__(self, other): Object to bitwise AND with the Series. Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -4312,7 +6049,8 @@ def __or__(self, other): Object to bitwise OR with the Series. Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -4350,7 +6088,8 @@ def __xor__(self, other): Object to bitwise XOR with the Series. Returns: - bigframes.series.Series: The result of the operation. + bigframes.pandas.Series: + The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -4365,6 +6104,7 @@ def __getitem__(self, indexer): >>> s = bpd.Series([15, 30, 45]) >>> s[1] np.int64(30) + >>> s[0:2] 0 15 1 30 @@ -4375,6 +6115,7 @@ def __getitem__(self, indexer): Index or slice of indices. Returns: - Series or Value: Value(s) at the requested index(es). + bigframes.pandas.Series or Value: + Value(s) at the requested index(es). """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/version.py b/third_party/bigframes_vendored/version.py index 0858c02c1e..50dde36b01 100644 --- a/third_party/bigframes_vendored/version.py +++ b/third_party/bigframes_vendored/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.32.0" +__version__ = "1.33.0"