R ile Veri Temizleme: dplyr ve tidyr Kullanımı

Analize başlamadan önce veriyi güvenilir ve tutarlı hâle getirmek gerekir. Eksik değerlerden tip dönüşümlerine, tekrar kayıtlardan tablo birleştirmeye kadar yaygın temizleme adımlarını gerçekçi R kodlarıyla ele alıyorum.

  • Araçlar

Veri temizleme neden kritik?

Veri temizleme, yalnızca boş hücreleri silmek değildir. Amaç; veri yapısını anlamak, iş kurallarına uymayan kayıtları belirlemek, veri tiplerini düzeltmek ve analiz boyunca korunması gereken bilgiyi kaybetmeden tutarlı bir tablo oluşturmaktır.

Örneğin sosyal medya raporunda aynı marka adı “Marka A”, “marka a” ve “MARKA A” olarak geçiyorsa gruplama sonuçları bölünür. Tarihler karakter olarak tutuluyorsa dönemsel analizler yanlış sonuç verir. Birleştirme anahtarları benzersiz değilse satır sayısı beklenmedik biçimde artar.

Temizleme akışı
  1. Veriyi
    Tanı
  2. Temizle
  3. Doğrula
  4. Analize
    Hazırla
Temel ilke: Her dönüşümün neden yapıldığını bilin. Mümkün olduğunda önce ve sonra satır sayılarını, eksik değerleri ve anahtarların benzersizliğini karşılaştırın. Ölçmediğiniz bir temizlik adımı, sessizce veri kaybettirebilir.

Gerekli paketler

dplyr satır ve sütun işlemleri ile birleştirmeleri; tidyr ise veriyi uzun/geniş forma dönüştürme ve dağınık hücreleri düzenleme işlerini kolaylaştırır. İkisi de tidyverse ekosisteminin parçasıdır.

Kurulum ve yükleme
install.packages("tidyverse")   # yalnızca ilk kurulumda
library(tidyverse)

Temizlemeden önce veriyi tanıyın

İlk adım, verinin boyutunu, sütun tiplerini ve örnek kayıtlarını incelemektir. Veriyi tanımadan yapılan otomatik düzeltmeler, geçerli kayıtların silinmesine veya anlamın bozulmasına yol açar.

İlk kontrol
glimpse(df)
dim(df)
names(df)
head(df, 10)
summary(df)

# Her sütundaki NA sayısı
df |>
  summarise(across(everything(),
                   ~ sum(is.na(.x))))

glimpse() sütun tiplerini ve örnek değerleri hızlıca gösterir. summary() sayısal alanların dağılımını, kategorik alanların ise frekans özetini anlamaya yardımcı olur.

Dikkat: Bir sütunun sayısal görünmesi, onun gerçekten sayısal olması gerektiği anlamına gelmez. Müşteri kimliği, posta kodu veya ürün kodu matematiksel bir büyüklük değilse karakter olarak tutulması daha doğrudur. Aksi hâlde baştaki sıfırlar kaybolur ve ortalaması alınabilen anlamsız bir alan elde edersiniz.

Eksik değerleri yönetme

R, eksik değerleri NA ile temsil eder. Eksik verinin neden oluştuğunu anlamak, hangi yöntemin seçileceğini belirler. Bir değişkende eksiklik rastgele olabilir, belirli bir kullanıcı grubunda yoğunlaşabilir veya veri toplama sürecindeki bir sorundan kaynaklanabilir.

Eksik değerleri sayma ve filtreleme

NA kontrolü
# Belirli sütun için eksik kayıt sayısı
sum(is.na(df$puan))

# Puanı dolu olan kayıtlar
df |>
  filter(!is.na(puan))

# Seçilen sütunların tamamı dolu olan kayıtlar
df |>
  drop_na(marka, tarih, puan)

drop_na(marka, tarih, puan) yalnızca bu üç sütundan en az biri eksik olan satırları dışarıda bırakır. Argüman verilmeden drop_na() kullanılırsa herhangi bir sütununda eksik değer bulunan satırlar elenir; bu işlem veri kaybını ciddi ölçüde artırır.

Eksikleri doldurmak

Kontrollü doldurma
# Metin alanındaki NA değerlerini etiketle
df |>
  mutate(kanal = replace_na(kanal, "Bilinmiyor"))

# Önceki gözlemi taşıma: sıralama çok önemlidir
df |>
  arrange(musteri_id, tarih) |>
  group_by(musteri_id) |>
  fill(segment, .direction = "down") |>
  ungroup()

fill() kullanırken önce arrange() ile doğru sıralamayı, sonra group_by() ile doğru sınırı kurmak zorunludur. Gruplama yapılmazsa bir müşterinin son bilinen segmenti, sıradaki başka bir müşterinin satırına taşınır.

Sayısal eksikleri ortalama veya medyan ile doldurmak bazı senaryolarda işe yarar; ancak dağılımı daraltır ve değişkenler arası ilişkileri çarpıtır. Doldurma yöntemi analitik amaca, eksikliğin nedenine ve değişkenin anlamına göre seçilmelidir. “Bilinmiyor” da gerçek bir kategori değil, veri kalitesi işareti olabilir.

Satırları filtreleme ve sıralama

filter() koşulları sağlayan kayıtları tutar. Birden fazla koşulda & “ve”, | “veya” anlamına gelir. Eksik değerler koşul değerlendirmesinde özel dikkat gerektirir.

Filtreleme örnekleri
# Puan eşiği
df |>
  filter(puan >= 70)

# Birden fazla kanal
df |>
  filter(kanal %in% c("Instagram", "YouTube"))

# Tarihe göre sıralama
df |>
  arrange(desc(tarih), marka)

filter(puan >= 70) ifadesi, puanı NA olan satırları sonuçta tutmaz: koşulun sonucu NA olduğunda satır seçilmez. Bu davranış genellikle istenendir, ama sessizdir. İş kuralı açısından eksik puanların ayrıca raporlanması gerekiyorsa bunları filtrelemeden önce ayrı bir kontrolle saymak iyi uygulamadır.

Sütun seçme, yeniden adlandırma ve dönüştürme

select() analize gerekli sütunları seçer, rename() adları değiştirir, mutate() yeni değişken üretir veya mevcut değişkenleri dönüştürür.

Sütunları düzenleme
temiz_df <- df |>
  select(kayit_id, marka, tarih, puan, kanal) |>
  rename(record_id = kayit_id,
         score     = puan) |>
  mutate(
    marka = str_squish(str_to_title(marka)),
    tarih = as.Date(tarih),
    score = as.numeric(score),
    puan_grubu = case_when(
      score >= 80          ~ "Yüksek",
      score >= 50          ~ "Orta",
      !is.na(score)      ~ "Düşük",
      TRUE                ~ "Eksik"
    )
  )

case_when() koşulları yukarıdan aşağıya değerlendirir ve ilk sağlanan koşulda durur. Son satırdaki TRUE yakalama kuralıdır; burada yalnızca score değeri NA olan satırlar oraya düşer, çünkü üçüncü koşul eksik olmayan tüm düşük puanları zaten almıştır.

Bu örnek, giriş sütunlarının uygun tiplerde olduğunu varsayar. Özellikle sayısal sütunda “yok”, “-” veya para birimi işaretleri varsa as.numeric() bunları sessizce NA yapar. Dönüşümden önce beklenmeyen değerleri kontrol edin.

Karakter verisini güvenli biçimde temizleme

Boşluk ve büyük/küçük harf
df |>
  mutate(
    marka  = str_squish(marka),
    marka  = str_to_lower(marka),
    eposta = str_trim(eposta)
  )

str_squish() baştaki ve sondaki boşlukları kaldırır, aradaki tekrarlı boşlukları teke indirir; str_trim() yalnızca uçlardaki boşlukları alır. Ancak her metin alanını küçük harfe çevirmek doğru değildir: özel kodlar, dosya adları veya büyük/küçük harfe duyarlı tanımlayıcılar bu işlemden zarar görür.

Türkçe karakter tuzağı: str_to_lower() ve str_to_upper() çalıştıkları yerel ayara (locale) duyarlıdır. “I” harfinin küçüğü Türkçede “ı”, İngilizcede “i”dir. Marka adlarını normalize ederken beklenmedik eşleşme hatalarıyla karşılaşmamak için locale argümanını bilinçli seçin.

Yinelenen kayıtları kontrol etme

Tekrar kayıt, veri setindeki iş tanımına göre değerlendirilmelidir. Aynı satırın birebir tekrarı ile aynı müşterinin birden fazla işlem yapması aynı şey değildir. Önce tablonun “bir satır neyi temsil ediyor?” sorusunu cevaplayın.

Tekrarları inceleme
# Birebir aynı satırları say
sum(duplicated(df))

# İş anahtarı varsayımı: kayıt kimliği benzersiz olmalı
df |>
  count(kayit_id) |>
  filter(n > 1)

# İş kuralı doğrulandıktan sonra benzersiz satırları tut
df |>
  distinct()
Önce kontrol, sonra silme: Tekrarları incelemeden distinct() uygulamayın. Aynı müşterinin aynı gün iki ayrı işlem yapması meşru olabilir. Hangi alanların benzersiz olması gerektiğini veri sözlüğü veya iş kuralı belirlemelidir; kod değil.

tidyr ile veriyi düzenleme

Analiz araçları çoğu zaman her değişkenin ayrı sütunda, her gözlemin ayrı satırda olduğu “tidy data” yapısından yararlanır. tidyr, verinin geniş ve uzun biçimleri arasında geçiş yapmayı ve bir hücreye sıkışmış bilgileri ayırmayı sağlar.

Geniş formdan uzun forma: pivot_longer()

Her ayın ayrı sütunda tutulduğu bir rapor tablosunu düşünelim. Grafik çizimi veya dönemsel karşılaştırma için ay ve değer sütunlarına dönüştürmek daha esnektir.

pivot_longer
aylik <- tibble(
  marka = c("A", "B"),
  ocak  = c(120, 90),
  subat = c(135, 110),
  mart  = c(150, 105)
)

uzun <- aylik |>
  pivot_longer(
    cols      = ocak:mart,
    names_to  = "ay",
    values_to = "mention_sayisi"
  )

Uzun formdan geniş forma: pivot_wider()

pivot_wider
genis <- uzun |>
  pivot_wider(
    names_from  = ay,
    values_from = mention_sayisi
  )

pivot_wider() kullanırken her marka-ay kombinasyonunun tek bir değere karşılık gelip gelmediğini kontrol edin. Aynı anahtar için birden fazla kayıt varsa tidyr hücreye liste yerleştirir ve uyarı verir; bu durumda values_fn ile bir özetleme fonksiyonu vermeniz ya da veriyi önceden toplulaştırmanız gerekir.

Bir sütunu bölme ve birleştirme

separate_wider_delim ve unite
kodlar <- tibble(kod = c("TR-IST-01", "TR-ANK-02"))

ayrik <- kodlar |>
  separate_wider_delim(
    kod, delim = "-",
    names = c("ulke", "sehir", "sube")
  )

birlesik <- ayrik |>
  unite(tam_kod, ulke, sehir, sube, sep = "-")

separate_wider_delim() güncel tidyr sürümlerinde ayırıcıya göre sütunları böler; eski projelerde separate() fonksiyonuna rastlayabilirsiniz. Ayırıcı metnin kendi içinde de bulunabiliyorsa önce biçimi doğrulayın, çünkü beklenen parça sayısı her kayıtta aynı olmayabilir. too_few ve too_many argümanları bu durumlarda ne yapılacağını açıkça belirtmenizi sağlar.

dplyr ile tabloları birleştirme

Birleştirme işlemleri, kayıtları ortak anahtarlar üzerinden ilişkilendirir. Doğru birleştirme için anahtar sütunlarının anlamı, veri tipleri ve benzersizlik düzeyi anlaşılmalıdır.

FonksiyonNe yapar?Tipik kullanım
left_join()Sol tablodaki tüm kayıtları korur.Ana tabloya boyut veya açıklama bilgisi ekleme.
inner_join()Yalnızca iki tabloda eşleşen kayıtları tutar.Her iki kaynakta da bulunan kayıtlarla çalışma.
full_join()İki tablodaki tüm kayıtları korur.Kaynaklar arasındaki eşleşmeyen kayıtları inceleme.
anti_join()İlk tabloda olup ikincide eşleşmeyenleri verir.Eksik eşleşmeleri ve anahtar sorunlarını bulma.
Join örneği ve anahtar kontrolü
paylasimlar <- tibble(
  marka_id = c(1, 2, 3),
  mention  = c(120, 85, 43)
)
markalar <- tibble(
  marka_id  = c(1, 2, 3),
  marka_adi = c("A", "B", "C")
)

# Sağ tablonun anahtarının benzersiz olduğunu doğrula
markalar |>
  count(marka_id) |>
  filter(n > 1)

sonuc <- paylasimlar |>
  left_join(markalar, by = "marka_id")

Sağ tabloda aynı marka_id birden çok kez bulunursa tek bir paylaşım kaydı birden fazla satırla eşleşir ve sonuç tablosu sessizce büyür. Bu, birleştirmelerde en sık yapılan hatadır. Güncel dplyr sürümlerinde beklenen ilişkiyi relationship argümanıyla açıkça belirtebilirsiniz; böylece varsayımınız bozulduğunda kod hata verir.

Eşleşmeyen anahtarları bulma
paylasimlar |>
  anti_join(markalar, by = "marka_id")

Bu çıktı, paylaşım tablosunda bulunan fakat marka tablosunda karşılığı olmayan anahtarları gösterir. Sonuç boş değilse nedeni eksik referans kaydı, farklı veri tipi (bir tarafta metin, diğerinde sayı), baştaki veya sondaki boşluk ya da farklı karakter kodlaması olabilir.

Uçtan uca temizleme akışı

Aşağıdaki örnek, gerçek projede kullanılabilecek bir işlem sırasını gösterir. Varsayım: raw_df içinde kayit_id, marka, tarih, puan ve kanal sütunları bulunuyor; tarih değerleri ISO biçiminde (YYYY-MM-DD), puanlar ise sayısal veya sayıya dönüştürülebilir değerlerden oluşuyor.

Yeniden üretilebilir temizleme hattı
clean_df <- raw_df |>
  mutate(
    marka = str_squish(marka),
    kanal = str_squish(kanal),
    tarih = as.Date(tarih),
    puan  = as.numeric(puan)
  ) |>
  filter(
    !is.na(kayit_id),
    !is.na(tarih)
  ) |>
  distinct() |>
  mutate(
    marka = na_if(marka, ""),
    kanal = replace_na(kanal, "Bilinmiyor")
  )

Bu bir başlangıç şablonudur. distinct() yalnızca birebir aynı satırları kaldırır, iş anahtarı bazlı tekilleştirme yapmaz. as.numeric() ise metin biçimindeki sayılarda uyarı üretip NA döndürebilir. Dönüşüm öncesi ve sonrası eksik değer sayılarını karşılaştırın; Türkiye biçimindeki ondalık virgüller için uygun yerel ayar ya da kontrollü karakter dönüşümü gerekir.

Kalite kontrolleri ekleyin

Son doğrulama
# Temizleme öncesi ve sonrası satır sayısı
nrow(raw_df)
nrow(clean_df)

# Tipler ve eksikler
glimpse(clean_df)
clean_df |>
  summarise(across(everything(),
                   ~ sum(is.na(.x))))

# Puan aralığı iş kuralına uygun mu?
clean_df |>
  filter(!is.na(puan), puan < 0 | puan > 100)

Son örnekteki 0–100 aralığı yalnızca puan değişkeni gerçekten bu ölçeği kullanıyorsa geçerlidir. Veri doğrulama kuralları her zaman alanın iş anlamına dayanmalıdır; genel geçer eşikler değil.

Yaygın hatalar için kontrol listesi

1

Satırın anlamı

Her satırın neyi temsil ettiğini ve benzersiz anahtarın ne olduğunu en başta tanımlayın.

2

Eksikler

Silmeden önce eksik değerlerin sayısını, oranını ve olası nedenlerini inceleyin.

3

Tipler

Metin, tarih ve sayısal sütunların tiplerini kontrol edin; başarısız dönüşümleri takip edin.

4

Anahtarlar

Birleştirmeden önce anahtarların benzersizliğini ve iki tablodaki veri tiplerini karşılaştırın.

5

Satır sayısı

Filtreleme, tekilleştirme ve birleştirme sonrasında satır sayısını yeniden doğrulayın.

6

İzlenebilirlik

Adımları betik hâlinde saklayın, ham veriyi değiştirmeden koruyun ve kararları belgeleyin.

Pratik öneri: Temizleme sürecini tek uzun komut olarak yazmak yerine mantıksal adımlara ayırın. Böylece hata ayıklamak, süreci yeniden çalıştırmak ve başkasına açıklamak kolaylaşır. Ara çıktıları adlandırmak, iki ay sonra kendi kodunuzu okurken de işe yarar.

Temiz veri, güvenilir analiz

dplyr ile satırları filtreleyebilir, sütunları düzenleyebilir, yeni değişkenler üretebilir ve tabloları anahtarlar üzerinden birleştirebilirsiniz. tidyr ise geniş ve uzun veri biçimleri arasında geçiş yapmanızı, hücrelere sıkışmış bilgileri ayrıştırmanızı sağlar.

En iyi sonuç, fonksiyonları ezberlemekten çok verinin yapısını anlamakla elde edilir. Her dönüşümün gerekçesini açıklayın, veri kaybını ölçün ve temizlenmiş veriyi analize almadan önce doğrulayın. Temizleme adımlarının çoğu geri alınamaz; bu yüzden ham veriyi her zaman olduğu gibi saklayın.

Why does data cleaning matter?

Data cleaning is not just deleting empty cells. The aim is to understand the structure of the data, identify records that break business rules, fix data types, and build a consistent table without losing information you need later in the analysis.

If the same brand appears as “Brand A”, “brand a” and “BRAND A” in a social media report, your grouped results split apart. If dates are stored as text, period comparisons give the wrong answer. If join keys are not unique, the row count grows unexpectedly.

The cleaning flow
  1. Profile
    the Data
  2. Clean
  3. Validate
  4. Prepare for
    Analysis
The basic principle: know why every transformation is being made. Wherever you can, compare row counts, missing values and key uniqueness before and after. A cleaning step you do not measure can quietly cost you data.

The packages you need

dplyr handles row and column operations and joins; tidyr makes it easier to move data between long and wide forms and to tidy up crowded cells. Both are part of the tidyverse.

Install and load
install.packages("tidyverse")   # only on first install
library(tidyverse)

Profile the data before you clean it

The first step is to inspect the size of the data, the column types and a few example records. Automatic fixes applied without knowing the data lead to valid records being deleted or meaning being destroyed.

First look
glimpse(df)
dim(df)
names(df)
head(df, 10)
summary(df)

# Number of NAs in each column
df |>
  summarise(across(everything(),
                   ~ sum(is.na(.x))))

glimpse() quickly shows column types and sample values. summary() helps you understand the distribution of numeric fields and the frequencies of categorical ones.

Careful: a column looking numeric does not mean it should be numeric. If a customer ID, postal code or product code is not a mathematical quantity, storing it as character is more correct. Otherwise leading zeros disappear and you end up with a meaningless field you can take the mean of.

Handling missing values

R represents missing values with NA. Understanding why the data is missing determines which method you should choose. Missingness in a variable can be random, concentrated in a particular user group, or caused by a problem in the collection process.

Counting and filtering missing values

Checking NAs
# Missing records in one column
sum(is.na(df$puan))

# Records where the score is present
df |>
  filter(!is.na(puan))

# Records complete across the selected columns
df |>
  drop_na(marka, tarih, puan)

drop_na(marka, tarih, puan) drops only the rows where at least one of those three columns is missing. Called without arguments, drop_na() removes every row with a missing value in any column, which can increase data loss dramatically.

Filling the gaps

Controlled filling
# Label NA values in a text field
df |>
  mutate(kanal = replace_na(kanal, "Unknown"))

# Carrying the previous observation forward: order matters
df |>
  arrange(musteri_id, tarih) |>
  group_by(musteri_id) |>
  fill(segment, .direction = "down") |>
  ungroup()

When using fill() you must first set the right order with arrange() and then the right boundary with group_by(). Without grouping, one customer's last known segment is carried into the next customer's row.

Filling numeric gaps with the mean or median works in some scenarios, but it narrows the distribution and distorts relationships between variables. The method should follow the analytical goal, the reason for the missingness and the meaning of the variable. “Unknown” may not be a real category either — it can be a data quality marker.

Filtering and sorting rows

filter() keeps the records that satisfy your conditions. With several conditions, & means “and” and | means “or”. Missing values need special attention when conditions are evaluated.

Filtering examples
# Score threshold
df |>
  filter(puan >= 70)

# Several channels
df |>
  filter(kanal %in% c("Instagram", "YouTube"))

# Sorting by date
df |>
  arrange(desc(tarih), marka)

filter(puan >= 70) does not keep rows where the score is NA: when a condition evaluates to NA, the row is not selected. That behaviour is usually what you want, but it is silent. If the business rule requires missing scores to be reported separately, count them in a dedicated check before filtering.

Selecting, renaming and transforming columns

select() picks the columns the analysis needs, rename() changes names, and mutate() creates new variables or transforms existing ones.

Tidying up columns
temiz_df <- df |>
  select(kayit_id, marka, tarih, puan, kanal) |>
  rename(record_id = kayit_id,
         score     = puan) |>
  mutate(
    marka = str_squish(str_to_title(marka)),
    tarih = as.Date(tarih),
    score = as.numeric(score),
    puan_grubu = case_when(
      score >= 80          ~ "High",
      score >= 50          ~ "Medium",
      !is.na(score)      ~ "Low",
      TRUE                ~ "Missing"
    )
  )

case_when() evaluates its conditions top to bottom and stops at the first match. The final TRUE is the catch-all; here only rows where score is NA fall through to it, because the third condition has already taken every non-missing low score.

This example assumes the input columns have suitable types. If a numeric column contains “none”, “-” or currency symbols, as.numeric() silently turns them into NA. Check for unexpected values before converting.

Cleaning character data safely

Whitespace and case
df |>
  mutate(
    marka  = str_squish(marka),
    marka  = str_to_lower(marka),
    eposta = str_trim(eposta)
  )

str_squish() removes leading and trailing whitespace and collapses repeated inner spaces into one; str_trim() only takes the ends. But lowercasing every text field is not right: special codes, file names and case-sensitive identifiers are damaged by it.

A Turkish character trap: str_to_lower() and str_to_upper() are locale-sensitive. The lowercase of “I” is “ı” in Turkish and “i” in English. When normalising brand names, choose the locale argument deliberately to avoid surprising match failures.

Checking for duplicate records

Duplicates must be judged against the business definition of the dataset. An exact repeat of the same row is not the same thing as one customer making several transactions. Answer the question “what does one row represent?” first.

Inspecting duplicates
# Count exactly repeated rows
sum(duplicated(df))

# Business key assumption: the record ID should be unique
df |>
  count(kayit_id) |>
  filter(n > 1)

# Keep unique rows once the rule is confirmed
df |>
  distinct()
Inspect first, delete second: do not run distinct() before examining the duplicates. The same customer making two separate transactions on the same day may be entirely legitimate. A data dictionary or business rule decides which fields must be unique — not the code.

Reshaping data with tidyr

Analysis tools usually benefit from “tidy data”, where each variable has its own column and each observation its own row. tidyr lets you move between wide and long forms and split information crammed into a single cell.

Wide to long: pivot_longer()

Think of a report table where every month sits in its own column. For plotting or period comparison, turning it into month and value columns is far more flexible.

pivot_longer
aylik <- tibble(
  marka = c("A", "B"),
  ocak  = c(120, 90),
  subat = c(135, 110),
  mart  = c(150, 105)
)

uzun <- aylik |>
  pivot_longer(
    cols      = ocak:mart,
    names_to  = "ay",
    values_to = "mention_sayisi"
  )

Long to wide: pivot_wider()

pivot_wider
genis <- uzun |>
  pivot_wider(
    names_from  = ay,
    values_from = mention_sayisi
  )

When using pivot_wider(), check whether every brand-month combination corresponds to a single value. If several records share a key, tidyr puts a list in the cell and warns you; you then need a summary function via values_fn, or you must aggregate beforehand.

Splitting and uniting a column

separate_wider_delim and unite
kodlar <- tibble(kod = c("TR-IST-01", "TR-ANK-02"))

ayrik <- kodlar |>
  separate_wider_delim(
    kod, delim = "-",
    names = c("ulke", "sehir", "sube")
  )

birlesik <- ayrik |>
  unite(tam_kod, ulke, sehir, sube, sep = "-")

separate_wider_delim() splits columns by a delimiter in current tidyr versions; in older projects you will meet separate(). If the delimiter can also appear inside the text, validate the format first, because the expected number of pieces may not be the same in every record. The too_few and too_many arguments let you state explicitly what should happen in those cases.

Joining tables with dplyr

Joins relate records through shared keys. For a correct join you need to understand the meaning of the key columns, their data types and their level of uniqueness.

FunctionWhat it doesTypical use
left_join()Keeps every record in the left table.Adding dimension or descriptive data to a main table.
inner_join()Keeps only records matched in both tables.Working with records present in both sources.
full_join()Keeps every record from both tables.Inspecting unmatched records across sources.
anti_join()Returns rows in the first table with no match in the second.Finding missing matches and key problems.
A join, with a key check
paylasimlar <- tibble(
  marka_id = c(1, 2, 3),
  mention  = c(120, 85, 43)
)
markalar <- tibble(
  marka_id  = c(1, 2, 3),
  marka_adi = c("A", "B", "C")
)

# Confirm the right-hand key is unique
markalar |>
  count(marka_id) |>
  filter(n > 1)

sonuc <- paylasimlar |>
  left_join(markalar, by = "marka_id")

If the same marka_id appears more than once in the right-hand table, a single mention record matches several rows and the result quietly grows. This is the most common mistake in joins. Current dplyr versions let you state the expected cardinality with the relationship argument, so the code fails when your assumption breaks.

Finding unmatched keys
paylasimlar |>
  anti_join(markalar, by = "marka_id")

This output shows keys present in the mentions table with no counterpart in the brand table. If the result is not empty, the cause may be a missing reference record, a type mismatch (text on one side, number on the other), leading or trailing whitespace, or a different character encoding.

An end-to-end cleaning pipeline

The example below shows an order of operations you could use in a real project. The assumption: raw_df contains kayit_id, marka, tarih, puan and kanal; dates are in ISO format (YYYY-MM-DD) and scores are numeric or convertible to numbers.

A reproducible cleaning pipeline
clean_df <- raw_df |>
  mutate(
    marka = str_squish(marka),
    kanal = str_squish(kanal),
    tarih = as.Date(tarih),
    puan  = as.numeric(puan)
  ) |>
  filter(
    !is.na(kayit_id),
    !is.na(tarih)
  ) |>
  distinct() |>
  mutate(
    marka = na_if(marka, ""),
    kanal = replace_na(kanal, "Unknown")
  )

This is a starting template. distinct() only removes exactly identical rows; it does not deduplicate by business key. as.numeric() can raise warnings and return NA for numbers held as text. Compare missing-value counts before and after the conversion; decimal commas in Turkish-formatted data need an appropriate locale or a controlled character conversion.

Add quality checks

Final validation
# Row counts before and after cleaning
nrow(raw_df)
nrow(clean_df)

# Types and missing values
glimpse(clean_df)
clean_df |>
  summarise(across(everything(),
                   ~ sum(is.na(.x))))

# Does the score respect the business range?
clean_df |>
  filter(!is.na(puan), puan < 0 | puan > 100)

The 0–100 range in that last example is valid only if the score variable really uses that scale. Validation rules should always rest on the business meaning of the field, never on generic thresholds.

A checklist for common mistakes

1

What a row means

Define at the outset what each row represents and what the unique key is.

2

Missing values

Before deleting, examine the count, the share and the likely causes of missingness.

3

Types

Check text, date and numeric column types, and track failed conversions.

4

Keys

Before joining, compare key uniqueness and data types across both tables.

5

Row counts

Re-verify the row count after filtering, deduplicating and joining.

6

Traceability

Keep the steps in a script, leave the raw data untouched, and document your decisions.

A practical suggestion: instead of writing the whole cleaning process as one long command, break it into logical steps. Debugging, re-running and explaining it to someone else all get easier. Naming the intermediate outputs also helps when you read your own code two months later.

Clean data, trustworthy analysis

With dplyr you can filter rows, organise columns, derive new variables and join tables on keys. tidyr lets you move between wide and long formats and pull apart information crammed into cells.

The best results come from understanding the structure of the data rather than memorising functions. Explain the reason for every transformation, measure the data you lose, and validate the cleaned data before taking it into analysis. Most cleaning steps cannot be undone, so always keep the raw data exactly as it came.