Você está na página 1de 31

Índice

Introdução ......................................................................................................................................................................... 4
O que é Spark? .................................................................................................................................................................. 4
O que é Databricks? .......................................................................................................................................................... 4
Delta lake ...................................................................................................................................................................... 4
Vamos ao PySpark! ........................................................................................................................................................... 5
O que importar? ............................................................................................................................................................ 5
Funções SQL .................................................................................................................................................................. 5
SQL no pyspark? Conheça o spark.sql ....................................................................................................................... 5
Como importar seu arquivo CSV, parquet, ou delta ................................................................................................. 6
Mais uma forma de importar os dados! ................................................................................................................... 7
Select - Escolha apenas as colunas que desejar........................................................................................................ 8
withColumn e select – Criação e edição de colunas ................................................................................................. 8
join, union e unionByName - Unindo dois dataframes ............................................................................................. 9
withColumnRenamed – Renomeando colunas......................................................................................................... 9
filter, where e like - Filtrando o dataFrame ............................................................................................................ 10
when – tratamento condicional de colunas ........................................................................................................... 11
groupBy e agg - Agrupamento de dados................................................................................................................. 12
orderBy – como ordenar seus dados ...................................................................................................................... 13
Tipos de dados ............................................................................................................................................................ 14
Cast + Select ............................................................................................................................................................ 14
Cast + withColumn .................................................................................................................................................. 14
printSchema – Descobrindo os tipos de dados....................................................................................................... 14
Lidando com colunas JSON ..................................................................................................................................... 15
Lidando com campos de data ................................................................................................................................. 16
Explode e explode_outer – campos com listas ....................................................................................................... 18
Funções Lambda e UDF ............................................................................................................................................... 19
Conhecendo as funções lambda e udf .................................................................................................................... 19
Aprofundando-se nessas funções ........................................................................................................................... 19
Algumas UDFs úteis................................................................................................................................................. 20
Window – Funções de janela ...................................................................................................................................... 21
Como definir uma window? .................................................................................................................................... 22
Funções Rankeamento de janelas (window ranking functions) ............................................................................. 22
Funções analíticas de janelas (window analytic functions) .................................................................................... 23
Funções de agregação de janelas (window aggregate functions) .......................................................................... 23
Cache e unpersist – otimizando o seu código............................................................................................................. 24
Exportação de arquivo ................................................................................................................................................ 25
Exportação Manual ................................................................................................................................................. 25
Salvar no userspace................................................................................................................................................. 25
Link com o Power BI ................................................................................................................................................ 26
Documentando seu código com %md ........................................................................................................................ 30
Considerações Finais ....................................................................................................................................................... 31
Introdução
Atenção, mochileiros, este pequeno guia contém o mínimo necessário para que possamos viajar em segurança pelo
Universo do Spark. Ao lê-lo, você alçará os mais altos voos nos obscuros cantos da galáxia do processamento em
paralelo. As explicações neste guia serão mostradas no contexto das bases da Juntos Somos Mais, e utilizarão como
base o Python (pyspark). Todos os códigos mostrados neste guia podem ser acessados no seguinte link:
https://cutt.ly/4b2uXCO

Tragam suas toalhas, e não entrem em pânico!

O que é Spark?
Em pouquíssimas palavras, Spark é um framework de execução, desenvolvido pela fundação Apache. Funciona através
de um cluster, que é um conjunto de computadores (nós), que dividem o trabalho de processamento entre eles,
fazendo com que os tempos de processamento de tarefas com grandes volumes de dados (Big Data) sejam muito mais
rápidos que os processamentos tradicionais. Spark foi escrito em Scala, mas suporta também as linguagens Python, R
e Java.

O que é Databricks?
Também em pouquíssimas palavras, o Databricks é uma plataforma de análise de dados, baseada em Spark, que
permite que se utilize o processamento em paralelo fornecido pelo framework e se escreva códigos organizados em
formatos de notebooks (para quem tem familiaridade com o Jupyter Notebook, a ideia é a mesma).

No Databricks é possível escrever códigos em SQL, Python, R, Java e Scala, e usar os clusters para se realizar o
processamento de seus códigos em paralelo.

Delta lake
A arquitetura de dados utilizada aqui na Juntos Somos Mais é o chamado Delta Lake. Nela os dados são separados em
camadas: os dados crus ficam na primeira camada, também chamada de Bronze layer, nesta camada os dados são
armazenados sem nenhum tipo de tratamento, e não há um schema enforcement. A Silver layer possui dados com
algum tipo de tratamento. Campos do tipo JSON, por exemplo, são separados em colunas ou tabelas, e as tabelas
possuem um schema definido.

Saiba mais: https://databricks.com/blog/2019/08/14/productionizing-machine-learning-with-delta-lake.html


Vamos ao PySpark!
O que importar?
Pyspark é uma biblioteca que transforma a linguagem de python em código Spark. As importações abaixo cobrem
grande parte das funções que se deseja cobrir em PySpark.

# Funções SQL
from pyspark.sql.functions import *
# Tipos de dados
from pyspark.sql.types import *
# Funções de Janela (window)
from pyspark.sql.window import Window
# User Defined Functions (UDF)
from pyspark.sql.functions import udf

Funções SQL
SQL no pyspark? Conheça o spark.sql
É possível ligar códigos SQL diretamente às bases do nosso Data Lake:

Exemplo:

user = spark.sql("""
SELECT u.*
FROM bronze_layer.sql_jsmidentityprd_dbo_customers c
JOIN bronze_layer.sql_jsmidentityprd_dbo_usercustomers uc ON c.Id = uc.CustomerId
JOIN bronze_layer.sql_jsmidentityprd_dbo_users u ON u.Id = uc.UserId
""")
display(user)

Além desta, é possível fazer os mais variados tipos de query utilizando diretamente as nossas bases.

Obs. Lembre-se sempre de mostrar a que banco de dados se refere na query (ex: bronze_layer, silver_layer,
gold_layer).
Como importar seu arquivo CSV, parquet, ou delta
No Databricks, é possível importar arquivos manualmente. Na homepage, utilize o campo abaixo para arrastar algum
arquivo que deseje utilizar:

Clique em Create Table In Notebook para acessar o código de criação do seu dataframe. Um notebook será aberto
com um bloco de código (cmd 2) similar ao código a seguir:

# File location and type


file_location = "/FileStore/tables/20210518_vendasAssaAbloy.csv"
file_type = "csv"

# CSV options
infer_schema = "true"
first_row_is_header = "true"
delimiter = ","

# The applied options are for CSV files. For other file types, these will
be ignored.
df = spark.read.format(file_type) \
.option("inferSchema", infer_schema) \
.option("header", first_row_is_header) \
.option("sep", delimiter) \
.load(file_location)

display(df)

Obs. Lembre-se de deixar os campos infer_schema = “true”, first_row_is_header = “true”, caso a primeira linha do
arquivo seja o header, e escolha o delimitador com a variável delimiter, caso seja um CSV.
Mais uma forma de importar os dados!
Outra forma de se importar os dados é acessando diretamente a tabela salva no nosso Data Lake, é possível
enxergar os caminhos dos arquivos através do Azure Storage Explorer.

O caminho deve ser escrito no formato abaixo:


abfss://bronze-layer@datahubstorageadlsprd.dfs.core.windows.net/market_b2b/jsmcache/customers/

A classe getDF está aqui para facilitar nossa vida:

class getDF:
## Converte os dados de uma tabela do databricks ou do Storage para Dataframe
def __init__(self,dbName,tableName,layer="bronze-
layer",source="market_b2b",formatTable="delta"):
"""
Salva um dataframe no storage (caso o arquivo já exista, ela dá um overwrite)
csv: fileType = com.databricks.spark.csv
parquet: fileType = parquet
folder: qual pasta do userspace será salva (no-billing, lv, crm, etc.)
"""

if dbName == "userspace":
path = "abfss://userspace@datahubstorageadlsprd.dfs.core.windows.net/" + source + "/
" + tableName + "." + formatTable + "/"
else:
path = "abfss://" + layer + "@datahubstorageadlsprd.dfs.core.windows.net/" + source
\
+ "/" + dbName + "/" + tableName + "/"
print(path)

self.df = spark.read.format(formatTable).load(path) #carrega df

Para chamar a classe, basta informar os parâmetros, como por exemplo:

Obs. Os parâmetros layer, source e formatTable estão como padrão na classe, se necessário, deve-se alterá-los para
acessar a base que precisar.

rawData = getDF('jsmsales', 'customers').df


display(rawData)

Também é possível buscar arquivos no nosso userspace (espaço reservado ao usuário), como no código a seguir:

df = getDF("userspace", "TransacaoDF", source="lv", formatTable="delta").df


Select - Escolha apenas as colunas que desejar
Agora que sabemos algumas formas de importar nossos dataframes, vamos aprender a utilizá-los da melhor forma.
Para escolher apenas algumas colunas, utiliza-se a função select.

Os argumentos da função podem ser o nome da coluna entre aspas, a expressão col(“nome_da_coluna”), ou no
formato df[“nome_da_coluna”]. Pode-se renomear uma coluna com a função alias(“novo_nome”).

O exemplo a seguir nos mostra algumas formas de utilização destas funções:

rawData = getDF('jsmsales', 'customers').df

rawData_renamed = rawData\
.select(\
"address", col("city"),\

rawData["CustomerCode"] ,\

col("cnpj").alias("novo_cnpj")

display(rawData_renamed)

Repare que é possível utilizar o símbolo da “\” para quebrar as linhas de código, e continuar escrevendo na linha
seguinte. O resultado é mostrado a seguir:

Obs1. Ao se utilizar a notação .select(“*”), seleciona-se todas as colunas de um dataframe.

withColumn e select – Criação e edição de colunas


Para criar novas colunas, pode-se utilizar tanto as funções withColumn quanto a função select. O exemplo a seguir
mostra como criar uma coluna calculada (cnpj_x_2) através do select, e duas colunas constantes através do
withColumn (coluna_constante e coluna_constante_2). Vale ressaltar que é possível criar colunas constantes e
calculadas tanto através do select quanto através do withColumn.

Obs1. A expressão lit() utilizada indica que o valor é um “literal”, deve ser utilizada para que o spark saiba diferenciar
constantes e variáveis de nomes de colunas. Quando se omite o lit(), o programa pode achar que se trata de um
nome de coluna.

Obs2. Ao usar withColumn() com o nome de uma coluna que já existe, esta será sobrescrita. Ao se fazer a mesma
coisa utilizando-se o select, uma nova coluna com o nome repetido será criada.

rawData_newColumns = rawData\
.select(\
"address", col("city"),"cnpj",\
(col("cnpj")*2).alias("cnpj_x_2")
)\
.withColumn("coluna_constante", lit("teste"))\
.withColumn("coluna_constante2", lit(0))

display(rawData_newColumns)
O código acima resulta no dataframe abaixo:

join, union e unionByName - Unindo dois dataframes


Para entender as funções join, union e unionByName, vamos antes aprender outra forma de criar dataFrames com o
pySpark.

df1 = spark.createDataFrame([[1, 2, 3]], ["col0", "col1", "col2"])


df2 = spark.createDataFrame([[1, 5, 9]], ["nome1", "nome2", "nome3"])
df3 = spark.createDataFrame([[2, 5, 1]], ["col1", "col2", "col0"])
df4 = spark.createDataFrame([[1, "teste1", "teste2"]], ["col0", "nomeCol1", "nomeCol2"])

#Primeiro Join
dfJoin1 = df1.join(df2, col("col0") == col("nome1"), "left")
#Segundo Join
dfJoin2 = df1.join(df3, ["col0","col1"], "left")
#Union
dfUnion = df1.union(df2)
#UnionByName
dfUnionByName = df1.unionByName(df3)

Resultado dos Joins 1 e 2:

Resultado do Union (repare que ele une as bases conforme a ordem das colunas):

Resultado do UnionByName (repare que ele une as bases conforme os nomes das colunas):

Como no SQL, há muitos tipos de join: left, right, full outer, e inner. Para escolher o tipo de join que será utilizado na
função, basta aplicar, no segundo argumento, alguma das seguintes: “left”, “right”, “full_outer”, “inner”. No PySpark,
entretanto, há um tipo de join diferente, o left_anti. Com esse tipo de join, você pega apenas os registros da tabela da
esquerda que não aparecem na tabela da esquerda.

withColumnRenamed – Renomeando colunas


Pode-se renomear colunas com a função withColumnRenamed, como abaixo:
rawDataRenamed = rawData_newColumns\
.withColumnRenamed("address","endereco")
display(rawDataRenamed)

O resultado abaixo mostra que a coluna ‘address’ foi renomeada para ‘endereco’

filter, where e like - Filtrando o dataFrame


Depois que selecionamos as colunas que vamos utilizar, é hora de filtrar os dados. Há algumas formas de fazê-lo, e
mostraremos algumas a seguir:

Filter:

#Filter 1
dfFilter1 = rawData_newColumns\
.filter("cnpj = '04753415000197' or cnpj = '24292922000188'")
display(dfFilter1)
#Filter 2 (utilizando o operador OR)
dfFilter2 = rawData_newColumns\
.filter((col("cnpj") == '04753415000197') | (col("cnpj") == '24292922000188'))
display(dfFilter2)
#Filter 3 (utilizando o operador AND)
dfFilter3 = rawData_newColumns\
.filter((col("cnpj") == '04753415000197') & (col("city") == 'SAO PAULO'))
display(dfFilter3)

Repare que se pode filtrar através de uma string no formato SQL (dfFilter1), ou através da notação de pyspark (os
operadores “and” e “or” são representados por & e |, respectivamente. Os outros operadores (==, !=, etc.) são iguais
aos do python. Os resultados do filter são mostrados a seguir:

Where:

A função where funciona de forma similar ao where do SQL, e sua notação é similar à do filter.
#Where 1
dfWhere1 = rawData_newColumns\
.where("cnpj is not null")
display(dfWhere1)
#Where 2 (utilizando o operador OR)
dfWhere2 = rawData_newColumns\
.where((col("cnpj") == '04753415000197') & (col("city") == 'SAO PAULO'))
display(dfWhere2)
#Where 3 (utilizando o operador AND)
dfWhere3 = rawData_newColumns.where(col("cnpj").isNotNull())
display(dfWhere3)
Veja que, para filtrar um campo por ser, ou não, nulo, pode-se utilizar tanto a expressão mostrada no dfWhere1
(“cnpj is not null” ou “cnpj is null”) quanto a expressão do dfWhere3 (col(“cnpj”).isNull() ou col(“cnpj”).isNotNull()).

Like:

A função like deve ser utilizada em conjunto com a where. Esta funciona da mesma forma que no SQL. Veja o
exemplo a seguir:

#Like
dfLike = rawData_newColumns\
.where(col("address").like("%RUA%"))
display(dfLike)

Esta consulta deverá retornar apenas as linhas cujo endereço possua a palavra ‘RUA’. O símbolo de % é utilizado
para indicar que não importa o que vem antes ou depois da palavra. Veja na coluna ‘address’, como todos os campos
possuem a palavra ‘RUA’.

when – tratamento condicional de colunas


Pode-se editar ou criar novas colunas com base em condições lógicas utilizando-se a função when. O código a seguir
é um exemplo de utilização da função dentro de um select().

truckView1 = getDF('jsmlogging', 'truckViewReport').df\


.select(\
"cnpj","StatusOrder",\
when((col("StatusOrder") == 'F') | (col("StatusOrder") == 'R') ,lit(1))\
.when((col("StatusOrder") == 'V') | (col("StatusOrder") == 'P'),lit(0))\
.otherwise((col("StatusOrder")).alias("isOrderOk")
)

É possível obter o mesmo resultado utilizando-se a função withColumn():

truckView2 = getDF('jsmlogging', 'truckViewReport').df\


.withColumn("isOrderOk",
when((col("StatusOrder") == 'F') | (col("StatusOrder") == 'R') ,lit(1))\
.when((col("StatusOrder") == 'V') | (col("StatusOrder") == 'P'),lit(0))\
.otherwise((col("StatusOrder")))
)\
.select("cnpj","StatusOrder","isOrderOk")
No primeiro caso, apenas as colunas “cnpj” e “statusOrder” são selecionadas, e a coluna “isOrderOk” é nomeada
através do .alias(). No segundo caso, o nome da coluna (isOrderOk) é o primeiro argumento da função, e o segundo
argumento representa a lógica que definirá o valor final. Ainda no segundo caso, é feito um select para pegar apenas
as colunas “cnpj”, “statusOrder” e “isOrderOk”, e gerar o mesmo resultado do primeiro.

Como funciona o when()?

Fazendo um paralelo com python, o primeiro when é equivalente ao “if”. Os .when() seguintes são equivalentes a
cada um dos “elif” que existiriam em python, pode-se usar quantos .when() quanto for necessário. O .otherwise() é
equivalente ao “else”, ou seja, se nenhuma das condições for atendida, o resultado final será esse.

Esse código indica que se a coluna statusOrder for igual a ‘F’ ou ‘R’, seu resultado será 1, caso seja ‘V’ ou ‘P’, seu
resultado é 0, caso contrário, o resultado será igual à coluna statusOrder. O resultado acima mostra algumas colunas
com StatusOrder = ‘V”, e a coluna isOrderOk = 0, na linha 534, entretanto, o StatusOrder é igual a ‘A’ e, portanto, cai
na regra do .otherwise(), fazendo que o resultado da isOrderOk também seja igual a ‘A’.

Obs. Sempre que há mais de uma condição, é necessário que as condições estejam entre parêntesis para que a
expressão funcione adequadamente.

groupBy e agg - Agrupamento de dados


Uma parte importantíssima de muitas análises são as funções de agrupamento. As funções groupBy e agg nos
auxiliarão nessa tarefa.

No exemplo a seguir, utilizaremos a base de pedidos (orders).

groupedData = orders\
.groupBy("cnpj","sellerId")\
.agg(\
count("salesOrder").alias("contagem"),\
countDistinct("PaymentFormCode").alias("contagem_distinta"),\
max("amountTotal").alias("valor_maximo"),\
min("amountTotal").alias("valor_minimo"),\
sum("amountTotal").alias("soma"),\
mean("amountTotal").alias("media"),\
first("amountTotal").alias("fist"),\
stddev("amountTotal").alias("desvio_padrao"),\
collect_set("PaymentFormDescription").alias("coleta_set"),\
collect_list("PaymentFormDescription").alias("coleta_lista"),\
percentile_approx("amountTotal", [0.25, 0.75, 0.9]).alias("approx_percent"),\
)
display(groupedData)

Através do groupBy, você pode decidir o grupo de variáveis que serão agrupadas, e através da agg, define-se que
função de agrupamento será utilizada. Para cada função de agrupamento, utilizou-se um alias para renomear a
coluna resultante, mas isto não é obrigatório. Cada função trará um resultado relacionado à coluna a que se refere.
As funções collect_set e collect_list juntam os dados da coluna em questão, sendo que a primeira não repetirá
registros, e a segunda repetirá, percentile_approx mostra os percentis 25%, 75% e 90% da coluna, agrupada por
CNPJ e sellerId. Vale observar que é possível também utilizar funções UDF neste tipo de agregação, mas trataremos
este tipo de função mais à frente no livro.

Tabelas dinâmicas (pivot tables)


No PySpark, há uma forma de “pivotar” linhas e colunas, simulando algo similar a uma tabela dinâmica do Excel.
Fazer isso é tão simples quanto utilizar a função pivot(), após um groupBy(), e depois definindo alguma função de
agregação.

dfUser = getDF("userspace", "TransacaoDF", source="lv", formatTable="delta").df

display(dfUser)

pivotDF = dfUser\
.groupBy("data_transacao")\
.pivot("status_transacao")\
.agg(countDistinct("numero_do_pedido"))

display(pivotDF)

No exemplo anterior, “pivotou-se” as linhas de de “status_transacao”, transformando-as nas colunas “aprovado” e


“negado”. Dentro do agg(), foi utilizado um countDistinct() para fazer a contagem distinta, por data, e para cada tipo
de status_transacao.

orderBy – como ordenar seus dados


Utilizando o mesmo dataFrame anterior, podemos ordenar os dados com relação a alguma das colunas, isso se dá
através da função orderBy().

A seguir veremos como podemos ordenar o dataframe groupedData em relação à coluna valor_maximo.

orderDFAsc = groupedData.orderBy("valor_maximo")
orderDFDesc = groupedData.orderBy(desc("valor_maximo"))

A primeira expressão ordenará os valores de forma crescente, e a segunda de forma decrescente.


Tipos de dados
É seguro dizer que um dos primeiros passos de uma boa análise de dados é saber com que tipo de dados estamos
lidando. Os dados são divididos entre:

• Numéricos
o ByteType(), ShortType(), IntegerType(), LongType(), FloatType(), DoubleType(), DecimalType()
• Strings
o StringType(), VarcharType(comprimento), CharType(comprimento)
• Binários
o BinaryType()
• Booleanos
o BooleanType()
• Datas
o TimestampType()
o DateType()
• Complexos
o StructType(campos)
▪ StructField(nome, tipoDeDado, nullable – indica se o valor pode ser null)

Documentação oficial do Spark: https://spark.apache.org/docs/latest/sql-ref-datatypes.html

Cast + Select
O código abaixo faz a definição de alguns tipos de dados utilizando a função cast() e select().

carts1 = getDF('jsmsales', 'carts').df\


.select(
col('Cnpj').cast(LongType()).alias('cnpj'),
col('CustomerCode').cast(LongType()).alias('customerCode'),
col('Finished').cast(BooleanType()).alias('finished'),
col('Status').cast(IntegerType()).alias('status')
)

Veja que transformamos os campos “cnpj” e “customerCode” em LongType(), “finished” em BooleanType(), e


“status” em IntegerType().

Cast + withColumn
Outra forma de modificar o tipo de dado é utilizando a função cast() dentro da withColumn(), por exemplo:

carts2 = getDF('jsmsales', 'carts').df\


.withColumn("cnpj", col("cnpj").cast(LongType()))\
.withColumn("CustomerCode", col("CustomerCode").cast(LongType()))\
.withColumn("Finished", col("Finished").cast(BooleanType()))\
.withColumn("Status", col("Status").cast(IntegerType()))\

printSchema – Descobrindo os tipos de dados


Para descobrir quais os tipos de dados (schema) presentes em um dataframe, deve-se utilizar a função
printSchema(), por exemplo:

carts1.printSchema()
Lidando com colunas JSON
JSON (JavaScript Object Notation) é um tipo de dado muito utilizado, principalmente quando se analisa fontes
resultantes de bancos de dados não relacionais (NoSQL). Em spark, o tipo JSON é geralmente tratado como um
StructType(), e com esse tipo de dados, podemos fazer diversos tratamentos.

Para exemplificar, faremos uma simples análise com as colunas cnpj, creationDate e seller, da nossa base de clientes.
O arquivo inicial é no formato a seguir:

Pode-se perceber que os campos creationDate e seller são JSONS. Como esse tipo de dado não é bom para análises,
vamos tirá-los de suas estruturas. O campo creationDate possui apenas um campo (chave) chamado $date, cujo valor
é um número (unix timestamp, estudaremos o tratamento de datas a seguir), e o campo seller possui os campos
creationDate (que é, por si só, um outro JSON), name, nameCompany, status, e _id.

Para acessar uma chave dentro de uma coluna JSON, pode-se utilizar a notação col(“coluna.chave”), como mostrado
a seguir:

rawData = getDF('jsmsales', 'customers').df\


.select("cnpj","CreationDate","seller")

treatedJSON = rawData\
.select("cnpj",\
col("CreationDate.$date").alias("creationDate"),\
col("seller.Name").alias("sellerName"),\
col("seller.NameCompany").alias("sellerNameCompany"),\
col("seller.CreationDate").alias("sellerDate"),\
)

display(treatedJSON)

Que resulta no seguinte dataframe:

Note que apenas as chaves contidas nos JSONs aparecem no novo dataframe. O campo sellerDate, entretanto, segue
sendo um JSON, pois era neste formato que estava anteriormente. Caso quiséssemos que aparecesse apenas o
número do campo sellerDate, deveríamos ter utilizado a notação col(“seller.CreationDate.$date”), neste caso,
acessaríamos o” último nível” deste campo, e restaria apenas o unix timestamp.

Outra forma de lidar com campos JSON é através da expressão col(“coluna.*”). Esta pequena linha de código fará com
que todos os campos dentro de um determinado JSON se tornem colunas individualmente. Podemos ver o resultado
no código a seguir:

rawData = getDF('jsmsales', 'customers').df\


.select("cnpj","CreationDate","seller")

treatedJSON2 = rawData\
.select("cnpj",\
col("CreationDate.$date").alias("creationDate"),\
col("seller.*")
)

display(treatedJSON2)

Ao invés de chamar cada chave da coluna seller individualmente, utilizamos apenas o col(“seller.*”), o resultado foi:

Cada campo tornou-se uma coluna separada. Este tipo de código pode ser muito útil quando nos deparamos com
JSONs com estruturas muito mais complexas que as que vimos neste exemplo.

Lidando com campos de data


Convertendo unix timestamp para datetime
Vimos, no exemplo anterior, que algumas datas podem ser apresentadas no formato unix timestamp, que é uma
contagem em milissegundos contados a partir do dia 01 de janeiro de 1970. A seguir lidaremos com os campos de
data da tabela anterior e os transformaremos em um formato mais “amigável”, o TimestampType().

dateDF = rawData\
.select("cnpj","creationDate")\
.withColumn("creationDate", from_unixtime(col("creationDate.$date")/1000))

display(dateDF)

Prontinho! Agora os campos estão num formato datetime, que contém a data e a hora.

Convertendo datetime em date (formato mais comum de data)


Há diversas formas de se expressar uma data, a mais comum no brasil é separando os temos com barras, no formato
dd/mm/YYYY (ex: 06/05/1995). Em bancos de dados, e códigos de programação em geral, entretanto, costuma-se
utilizar a notação da ISO 8601, onde a data vai do maior intervalo de tempo (ano) até o menor (dia), separando os
termos com “-“, ficando no formato YYYY-mm-dd.

O PySpark nos permite facilmente converter campos no formato timestamp em datas, e isso é possível graças ao
to_date(), que podemos ver no exemplo a seguir:

dateDF3 = dateDF2.withColumn("date", to_date("creationDate"))


display(dateDF3)

Note que todos os timestamps da coluna creationDate tornaram-se datas no formato ISSO 8601, na coluna date.

Mudando o fuso-horário de uma data


Muitas vezes, um campo de data encontra-se num outro fuso-horário, como o UTC (Universal Timestamp
Coordinated). Pode-se converter estes campos para o nosso fuso-horário com o seguinte código:

dateDF2 = dateDF\
.withColumn("creationDate", from_utc_timestamp(col("creationDate"),"America/Sao_Paulo"))

Note que todas as datetimes foram reduzidas em 3 horas em relação ao dataframe anterior.

Algumas operações com datas: date_add(), date_sub() e datediff()


É possível somar e subtrair dias das datas através das funções date_add() e date_sub(), respectivamente. A função
datediff mostra qual a diferença entre duas datas, em dias.

dateDF4 = dateDF3\
.withColumn("sumDate", date_add("date", 10))\
.withColumn("subDate", date_sub("date", 10))\
.withColumn("dateDiff", datediff("sumDate","subDate"))

display(dateDF4)

Obs. Outra forma de subtrair datas, é através da mesma função date_add(), mas inserindo um índice negativo no
segundo argumento.
Explode e explode_outer – campos com listas
Muitas vezes nos deparamos com dados em formatos de listas (ou arrays). Existem várias formas de tratar tais
dados, mas agora abordaremos apenas duas delas: explode e explode_outer.

O comando explode simplesmente pega uma coluna de listas e “explode” todos os elementos dessa lista, colocando
um em cada linha. Vale ressaltar que, caso a lista de uma linha seja vazia, esta não aparecerá quando utilizarmos o
explode. Para que se tenha todas as linhas, independente de as listas serem vazias ou não, deve-se utilizar o
comando explode_outer.

Para o exemplo, criaremos um dataframe de testes, que conterá uma coluna de lista:

dfExplode = spark.createDataFrame([\
["Lucas", 27, ["Morango","Pera","Uva"]],
["Carol", 26, ["Pitaya","Pequi","Pistache"]],
["Maria", 30, []]],\
["pessoa", "idade", "fruta"])

Veja o que acontece se utilizarmos a função explode no dataframe acima:

dfExplode1 = dfExplode.select("pessoa","idade", explode("fruta").alias("fruta"))

Note que cada elemento da lista foi transferido para uma linha, e todos os outros valores se repetiram. Como
utilizamos explode, e o campo fruta de Maria estava vazio, ela não aparece nesse segundo dataframe.

Mostraremos agora o que acontece quando se utiliza o comando explode_outer:

dfExplode2 = dfExplode.select("pessoa","idade", explode_outer("fruta").alias("fruta"))

Agora Maria também aparece no dataframe, mas seu campo fruta está como null.
Funções Lambda e UDF
Conhecendo as funções lambda e udf
O conceito das expressões lambda é extremamente importante em Python. Trata-se basicamente de atribuir uma
função a um objeto. Abaixo segue um exemplo simples de uma função lambda:

#A função abaixo
def func(x,y):
return x + y

#Pode ser escrita como:


func = lambda x,y: x + y

Em spark, para se utilizar uma função lambda, é necessário primeiro entender o conceito de UDF (User Defined
Function). Sempre que o usuário quiser fazer um tratamento personalizado utilizando alguma função que não é
nativa do Spark, deve-se chamar o udf.

Segue exemplo simples de aplicação de udf e lambda em Spark:

orders = getDF('jsmsales', 'myOrders').df\


.select("cnpj",\
col("AmountBilled").cast(DoubleType()),\
col("AmountPendent").cast(DoubleType()),\
)\
.filter("AmountBilled is not null and AmountPendent is not null")

lambdaSoma = udf(lambda x, y, z: x + y + z)

orderLambda = orders\
.withColumn("colunaSoma",\
lambdaSoma("AmountBilled","AmountPendent",lit(1000))
)

display(orderLambda)

No exemplo acima, primeiro foi aplicado o conceito de tipo de dado que aprendemos anteriormente, e transformou-
se as colunas amountBilled e amountPendent em DoubleType(). Foi também aplicado um filtro para que nenhum dos
valores das colunas analisadas fossem nulos. Logo em seguida foi criada a função lambdaSoma utilizando-se uma UDF,
para que o spark reconheça as colunas como parâmetros. O próximo passo foi utilizar a withColumn para passar
parâmetro as colunas que se desejava. Repare que, no caso, passamos duas colunas e um literal como parâmetros x,
y e z. A coluna calculada (colunaSoma) foi resultado da soma de cada uma das colunas + 1000.

Obs1. Vale lembrar que, para se passar um valor literal como parâmetro de uma função, é necessário usar o lit().

Obs2. Ao invés de withColumn, poderia também ser usada a função select.

Aprofundando-se nessas funções


Agora que conhecemos o básico sobre funções lambda e udf, estamos prontos para criar funções (um pouco) mais
complexas, e tratar dados de forma mais personalizada.
Na prática, não há tanta diferença entre o que faremos a seguir e o que fizemos anteriormente. Criaremos nossa
própria UDF, e depois a transformaremos em uma lambda. Segue exemplo:

cityNames = getDF('jsmcache', 'customers').df\


.select("city")\
.where(col("city").like("%Ã%") | col("city").like("%Ó%"))

def treatName(name):
#trata o nome da cidade para retirar caracteres especiais
name = name.upper()\
.replace("Á","A")\
.replace("Ã","A")\
.replace("Ó","O")\
return name.strip()

treatCityName = udf(lambda x: treatName(x))

city2 = cityNames\

.select("*",\

treatCityName("city").alias("newCityName")

display(city2)

A função treatCityName() acima é um exemplo de udf(lambda) que retira alguns caracteres especiais de uma
determinada string. A coluna newCityName contém os nomes das cidades sem esses caracteres especiais, como
mostrado abaixo:

Os exemplos mostrados acima, apesar de extremamente simples, abrem portas para um conhecimento muito mais
profundo, que permite que tratemos os dados em Spark utilizando não somente as funções nativas do framework,
mas também as incontáveis funções que podemos criar de forma personalizada. Dominar o conceito de lambda e
UDF em spark é um importante passo para conseguir, cada vez mais, realizar análises muito mais complexas.

Algumas UDFs úteis


Diferença entre datas em dias úteis
import numpy as np

workdaysUDF = udf(lambda date1, date2: int(np.busday_count(date2, date1)) if (date1 is not


None and date2 is not None) else None, IntegerType())

udf1 = dateDF4.withColumn("workdays", workdaysUDF("sumDate","subDate"))


display(udf1)
Cria uma coluna com um número long aleatório
import random

loopRandInt = udf(lambda x: random.randrange(1,9223372036854775807))


udf2 = udf1.withColumn("randInt",loopRandInt("cnpj"))
display(udf2)

Criptografa um número qualquer de colunas em MD5

def encrypt_column(*args):
colName = ""
for i in args:
colName = colName + str(i)
try:
sha_value = colName
return sha_value
except Exception as e:
return None
loopMD5 = udf(lambda *x: encrypt_column(*x))

udf3 = udf2.withColumn("md5", md5(loopMD5("cnpj","creationdate","date","randInt")))

display(udf3)

Window – Funções de janela


As Windows são funções que determinam uma “janela” de dados que se deseja analisar, podendo-se agrupar e
ordenar dados da forma que for mais conveniente. Existem três tipos principais de funções window: ranking,
analíticas e de agregação.
Como definir uma window?
O primeiro passo é definir qual o particionamento, o ordenamento, e o alcance, sendo que nenhum desses
parâmetros é obrigatório, desde que haja pelo menos um deles.

• partitionBy(*cols)
o Coluna(s) que particionarão a janela (como agrupar esses dados)
• orderBy(*cols)
o Coluna(s) que definem a ordem da janela
o Pode ser ascendente ou descentente (asc(“col”) ou desc(“col”))
• rangeBetween(início,fim)
o Define qual o alcance considerado na janela, ordenados pela expressão orderBy().
o Os valores de início e fim são relativos à linha atual. Por exemplo, “0” significa “linha atual”,
enquanto “-1” significa um registro antes, e assim por diante.
o Os valores podem ser, além de números, os seguintes:
▪ Window.unboundedPreceding – Todos os valores anteriores à linha atual
▪ Window.unboundedFollowing – Todos os valores seguintes à linha atual
▪ Window.currentRow – Linha atual

A base que utilizaremos para a nossa análise será a seguinte:

orders = getDF('jsmsales', 'myOrders').df\


.withColumn("date", from_unixtime(col("CreateDate.$date")/1000))\
.select("date","cnpj",\
col("sellerId.$binary").alias("sellerId"),\
"amountTotal")

O código a seguir mostra uma “window” de uma tabela de vendas:

windowval = Window\
.partitionBy("cnpj","sellerId")\
.orderBy("date")\
.rangeBetween(Window.unboundedPreceding, 0)

Neste exemplo, o particionamento é pelas colunas cnpj e sellerId, ordenados pela data, com o range considerado de
todos os registros anteriores à linha até a data atual.

Funções Rankeamento de janelas (window ranking functions)


Funções de Ranking, como o próprio nome sugere, servem para ordenar dados e ranqueá-los.

rank(): qual a posição absoluta em relação à coluna de ordenamento. Duplica o rank de valores iguais

dense_rank(): igual ao rank() em relação às duplicatas, mas remove os gaps entre cada rank
percent_rank(): qual a posição percentual em relação à coluna de ordenamento.

windowval = Window.partitionBy("Cnpj","sellerId").orderBy("amountTotal")

rankDF = orders\
.withColumn("ranking", rank().over(windowval))\
.withColumn("dense_rank", dense_rank().over(windowval))\
.withColumn("percent_rank", percent_rank().over(windowval))

Na dataframe acima, podemos ver um rankeamento particionado pelas colunas cnpj e sellerId, e ordenados pelo
amountTotal.

Funções analíticas de janelas (window analytic functions)


As funções analíticas são:

cume_dist: distância cumulativa

lag: retorna o valor de uma linha anterior

lead: retorna o valor de uma linha seguinte

Exemplo de utilização:

windowval2 = Window.partitionBy("Cnpj","sellerId").orderBy("date")

analyticDF = orders\
.withColumn("cume_dist", cume_dist().over(windowval2))\
.withColumn("lag", lag("amountTotal",1).over(windowval2))\
.withColumn("lead", lead("amountTotal",2).over(windowval2))

Percebe-se que, na coluna lag, aparece o valor de amountTotal da data anterior, e na coluna lead, o valor de
amountTotal de duas linhas depois. Isso acontece devido ao argumento 2 passado na função de lead.

Funções de agregação de janelas (window aggregate functions)


É possível utilizar as Windows com as mesmas funções de agregação que vimos anteriormente (sum, mean, max,
etc.)

O exemplo abaixo mostra a soma acumulada do amountTotal:

windowval3 = Window\
.partitionBy("Cnpj","sellerId")\
.orderBy("date")\
.rangeBetween(Window.unboundedPreceding, 0)

aggregatedDF = orders\
.withColumn("somaAcumulada", sum("amountTotal").over(windowval3))

Este foi apenas uma das infinitas aplicações das funções de window de agregação. Pode-se fazer um trabalho similar
para analisar muitos outros fatores usando essa importante ferramenta do Spark.

Cache e unpersist – otimizando o seu código


O Spark é um framework de processamento em memória, o que significa que, quando você declara determinada
variável, ela não é armazenada em disco. A implicação prática disso é um processamento muito mais rápido que as
alternativas convencionais baseadas em disco.

Um ponto a se atentar, entretanto, é que quando se utiliza uma mesma variável (dataframe, por exemplo) diversas
vezes seguidas, o spark processa esse dataframe em memória todas as vezes que a esta for utilizada, o que pode incutir
num maior tempo de processamento.

A alternativa a esse problema é guardar o dataframe em disco, caso ele seja utilizado muitas vezes, e demande muito
tempo de processamento. O processo se dá através da função cache().

A seguir, criaremos um dataframe (rawUser) e contaremos a quantidade de linhas contida nele.

rawUser = spark.sql("""
SELECT u.*
FROM bronze_layer.sql_jsmidentityprd_dbo_customers c
JOIN bronze_layer.sql_jsmidentityprd_dbo_usercustomers uc ON c.Id = uc
.CustomerId
JOIN bronze_layer.sql_jsmidentityprd_dbo_users u ON u.Id = uc.UserId
""")

print(rawUser.count())

Todas as vezes que rodamos este comando, o cluster demora aproximadamente 4.49 segundos para contar a
quantidade de linhas, como mostrado no print abaixo:

Faremos o código de forma diferente, desta vez utilizando a função cache(), que guardará o dataframe em disco. É
importante ressaltar que todas as vezes que se usa essa função, deve-se chamar imediatamente após alguma outra
função que percorrerá o dataframe, como a count, por exemplo. O código fica como a seguir:

rawUser = spark.sql("""
SELECT u.*
FROM bronze_layer.sql_jsmidentityprd_dbo_customers c
JOIN bronze_layer.sql_jsmidentityprd_dbo_usercustomers uc ON c.Id = uc
.CustomerId
JOIN bronze_layer.sql_jsmidentityprd_dbo_users u ON u.Id = uc.UserId
""").cache()
rawUser.count()

A primeira vez que usamos esse comando, o tempo de processamento irá demorar mais do que quando não o
utilizamos, nesse caso, o comando durou 10.79 segundos.

O ponto interessante, entretanto, ocorre quando damos um rawUser.count() novamente depois de guardarmos o
dataframe em disco, o tempo de processamento é de 0.29 segundos.

Nota-se que, apesar de ter demorado mais da primeira vez que usamos a função, em todas as vezes que utilizarmos o
dataframe a seguir demandarão muito menos tempo. Vale ressaltar que a utilização da função cache() só faz sentido
em cenários em que se utilizará um mesmo dataframe diversas vezes, e sempre deve ser ponderado o tempo extra
que será perdido da primeira vez que se dá o cache().

No fim do seu código, não se esqueça de apagar o dataframe da memória, quando não for mais utilizá-lo.

Faça isso através da função unpersist(). É importantíssimo que se dê o unpersist(), pois caso contrário, seu dataframe
ocupará espaço em disco do cluster, e pode deixá-lo sobrecarregado. A utilização desse comando é tão simples quanto:

rawUser.unpersist()

Exportação de arquivo
OK, agora você já sabe diversas formas de tratar e se divertir com seus arquivos, como pode salvá-los ou
compartilhá-los com alguém?

Exportação Manual
A forma mais simples de se exportar um dataframe é através clicando no botão de download que fica no cmd quando
você dá um display() num dataframe. Caso o dataframe possua mais de 1.000 linhas, deve-se clicar na seta ao lado e
selecionar a opção “Download full results (max 1 million rows)”, como na figura a seguir. Não custa reafirmar, esta
opção exportará dataframes de até no máximo 1 milhão de linhas, o resultado será baixado no formato CSV.

Salvar no userspace
Como dito anteriormente, o userspace é um local onde os usuários podem salvar e ler arquivos. Mas cuidado, os
arquivos salvos neste local são apagados mensalmente, então não o utilize para criar bases perenes. O ambiente deve
ser utilizado apenas para exportações pontuais.

A classe saveDF foi criada para facilitar o processo de salvamento de dados no userspace.
class saveDF:
def __init__(self,dataFrame,dbName,tableName,layer="bronze-
layer",source="market_b2b",formatTable="delta"):
"""
Salva um dataframe no storage
csv: fileType = com.databricks.spark.csv
parquet: fileType = parquet
folder: qual pasta do userspace será salva (no-billing ou lv)
"""

if dbName == "userspace":
path = "abfss://userspace@datahubstorageadlsprd.dfs.core.windows.net/" + so
urce + "/" + tableName + "." + formatTable + "/"

try:
dataFrame.write.mode("overwrite").format(formatTable)\
.option("header", "true").option("overwrite", "true")\
.option("overwriteSchema", "true").save(path)
print("Arquivo salvo: \n %s" %(path))
except BaseException as e:
print('Erro no salvamento: ' + str(e))

O código abaixo está salvando o dataframe dfLike no formato delta na pasta lv, que fica dentro do userspace. O nome
do arquivo salvo será “dfTeste”.

saveDF(dfLike,"userspace","dfTeste",source="lv",formatTable="delta")

Para utilizar o arquivo, basta chamar a função getDF(), que já foi apresentada anteriormente:

dfLike = getDF("userspace", "dfTeste", source="lv", formatTable="delta").df

Link com o Power BI


Uma boa forma de compartilhar seus dados com o time é através do link com o powerBI. Para isso utilizaremos
alguns dos conceitos aprendidos até agora, e alguns novos. O primeiro passo é ter uma tabela à qual o Power BI será
conectado.

Atenção: o ideal é que se utilize os dados da camada gold no databricks, o trabalho de criação e gestão das tabelas é
do time de engenharia de dados. Esta solução pode ser utilizada apenas de forma temporária, enquanto nossas
bases não estão disponíveis.

Para criar uma tabela, é necessário que se crie um banco de dados, caso não exista. O que pode ser feito através do
comando:

%sql
create database lv;

Utiliza-se o comando %sql para que o databricks entenda que o comando a seguir segue o mesmo formato. O código
acima cria um banco de dados chamado lv, que pode ser visto na seção de dados do databricks.
Agora é possível criar uma tabela:

%sql
create table lv.CoracaoDeOuro using delta location 'abfss://userspace@datahubstorageadlsp
rd.dfs.core.windows.net/lv/dfTeste.delta/';

Criamos uma tabela a partir do arquivo dfTeste, que salvamos na seção anterior, e a chamamos de lv.CoracaoDeOuro.
Agora que criamos nossa primeira tabela, estamos prontos para fazer a conecção com o databricks.

No Power BI, clique em Obter dados → Mais..., como na imagem a seguir:

Um pop-up será aberto, clique em Azure → Azure Databricks, e clique em conectar.

De volta ao Databricks, clique em clusters, escolha o cluster que estiver utilizando, e vá a Advanced Options. Nesta
aba aparecerão os dois campos que você deverá inserir no powerBI. Cole o Server Hostname e o HTTP Path em seus
respectivos locais no power BI. Uma tela de navegação aparecerá, escolha o banco que criamos, bem como a tabela,
e clique em conectar.

Clique em carregar, e aguarde enquanto as bases selecionadas são carregadas, e prontinho!

Para publicar seu power BI para a versão online (é necessário ter uma licença premium para isso), clique em
“Publicar”, no canto superior direito da tela.

No site do Power BI, vá à seção lateral onde estão todos os painéis, no nosso caso, o painel foi salvo na aba Loja Virtual:
Procure o dataset que salvou, clique nos 3 pontos ao lado dele, depois em Settings:

Para definir uma rotina de atualização, clique em “scheduled refresh”, “add another time” e defina uma frequência e
horários para que seu painel seja atualizado, depois clique em apply:

O último passo é editar suas credenciais, clicando em “data source credentials” e “edit credentials”, selecionae
authentication method = key, e insira o seu token do databricks no campo account key. Seu painel agora será
atualizado automaticamente todos os dias às 7:30 da manhã.
Documentando seu código com %md
Caso não tenha notado, no notebook do Guia do Mochileiro do Spark há um índice do lado esquerdo da tela.

Para criar esse índice, basta escrever, num cmd qualquer, a expressão %md. Ao escrever algo após o %md, quanto
mais “#”, menor será o tamanho do título.

Exemplo:

%md #Título Grande

%md ##Título um pouco menor

%md ###Título menor ainda

%md ####Título menor que o anterior

Caso tenha interesse em conhecer a documentação completa do %md, pode encontra-la neste link.
Considerações Finais
Esta é uma primeira edição deste guia, futuramente esperamos acrescentar mais informações sobre a biblioteca
SparkML (Machine Learning com Spark), e acrescentar outras funções que forem importantes. Esperamos que tenham
aproveitado a viagem, e que este pequeno guia possa te levar a lugares cada vez mais altos.

Um grande abraço!

Tripulação da Loja Virtual da Juntos Somos Mais.

Você também pode gostar